deck improves:

* gui: removed public deck hash info;
* gui: improved xmage dck-file - now it correctly load a card's amount (related to files from third party services);
* server: fixed wrong cheating warning on deck construction (closes #11877);
* refactor: removed outdated hash code and calculations;
* other: added docs, added multiple deck hash tests;
This commit is contained in:
Oleg Agafonov 2024-04-10 22:18:07 +04:00
parent 889c1125e8
commit 7817a5cac6
32 changed files with 551 additions and 247 deletions

View file

@ -87,7 +87,6 @@ import java.util.prefs.Preferences;
public class MageFrame extends javax.swing.JFrame implements MageClient {
private static final String TITLE_NAME = "XMage";
private static final Logger logger = Logger.getLogger(MageFrame.class);
private static final Logger LOGGER = Logger.getLogger(MageFrame.class);
private static final String LITE_MODE_ARG = "-lite";
@ -398,7 +397,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
}
private void bootstrapSetsAndFormats() {
logger.info("Loading sets and formats...");
LOGGER.info("Loading sets and formats...");
ConstructedFormats.ensureLists();
}
@ -1460,7 +1459,7 @@ public class MageFrame extends javax.swing.JFrame implements MageClient {
try {
instance = new MageFrame();
} catch (Throwable e) {
logger.fatal("Critical error on start up, app will be closed: " + e.getMessage(), e);
LOGGER.fatal("Critical error on start up, app will be closed: " + e.getMessage(), e);
System.exit(1);
}

View file

@ -1861,9 +1861,9 @@ public class DragCardGrid extends JPanel implements DragCardSource, DragCardTarg
List<CardView> gridStack = new ArrayList<>();
gridRow.add(gridStack);
for (DeckCardInfo info : stack) {
if (trackedCards.containsKey(info.getSetCode()) && trackedCards.get(info.getSetCode()).containsKey(info.getCardNum())) {
if (trackedCards.containsKey(info.getSetCode()) && trackedCards.get(info.getSetCode()).containsKey(info.getCardNumber())) {
List<CardView> candidates
= trackedCards.get(info.getSetCode()).get(info.getCardNum());
= trackedCards.get(info.getSetCode()).get(info.getCardNumber());
if (!candidates.isEmpty()) {
gridStack.add(candidates.remove(0));
thisMaxStackSize = Math.max(thisMaxStackSize, gridStack.size());

View file

@ -328,7 +328,7 @@ public class DeckGeneratorDialog {
tmp.getParentFile().mkdirs();
tmp.createNewFile();
deck.setName(deckName);
XMAGE.getExporter().writeDeck(tmp.getAbsolutePath(), deck.getDeckCardLists());
XMAGE.getExporter().writeDeck(tmp.getAbsolutePath(), deck.prepareCardsOnlyDeck());
cleanUp();
return tmp.getAbsolutePath();
} catch (Exception e) {

View file

@ -794,18 +794,14 @@ public class DeckEditorPanel extends javax.swing.JPanel {
dialog.showDialog();
if (!dialog.getTmpPath().isEmpty()) {
Deck deckToAppend = null;
StringBuilder errorMessages = new StringBuilder();
MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR));
try {
deckToAppend = Deck.load(DeckImporter.importDeckFromFile(dialog.getTmpPath(), errorMessages, false), true, true);
Deck deckToAppend = Deck.load(DeckImporter.importDeckFromFile(dialog.getTmpPath(), errorMessages, false), true, true);
processAndShowImportErrors(errorMessages);
if (deckToAppend != null) {
deck = Deck.append(deckToAppend, deck);
this.deck = Deck.append(deckToAppend, this.deck);
refreshDeck();
}
} catch (GameException e1) {
JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE);
} finally {
@ -864,7 +860,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR));
try {
DeckFormats.writeDeck(needFileName, deck.getDeckCardLists());
DeckFormats.writeDeck(needFileName, deck.prepareCardsOnlyDeck());
try {
MageFrame.getPreferences().put("lastExportFolder", file.getCanonicalPath());
@ -1364,7 +1360,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
useDeckInfo = true;
}
MageFrame.getDesktop().setCursor(new Cursor(Cursor.WAIT_CURSOR));
DeckCardLists cardLists = deck.getDeckCardLists();
DeckCardLists cardLists = deck.prepareCardsOnlyDeck();
cardLists.setCardLayout(deckArea.getCardLayout());
cardLists.setSideboardLayout(deckArea.getSideboardLayout());
if (!useDeckInfo) {
@ -1444,12 +1440,14 @@ public class DeckEditorPanel extends javax.swing.JPanel {
if (mode == DeckEditorMode.SIDEBOARDING
|| mode == DeckEditorMode.LIMITED_BUILDING
|| mode == DeckEditorMode.LIMITED_SIDEBOARD_BUILDING) {
// in game mode - move all to sideboard
for (Card card : deck.getCards()) {
deck.getSideboard().add(card);
}
deck.getCards().clear();
cardSelector.loadSideboard(new ArrayList<>(deck.getSideboard()), this.bigCard);
} else {
// in deck editor mode - clear all cards
deck = new Deck();
}
refreshDeck();
@ -1488,7 +1486,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
updateDeckTask.cancel(true);
}
if (SessionHandler.submitDeck(mode, tableId, deck.getDeckCardLists())) {
if (SessionHandler.submitDeck(mode, tableId, deck.prepareCardsOnlyDeck())) {
removeDeckEditor();
}
}//GEN-LAST:event_btnSubmitActionPerformed
@ -1504,7 +1502,7 @@ public class DeckEditorPanel extends javax.swing.JPanel {
updateDeckTask.cancel(true);
}
if (SessionHandler.submitDeck(mode, tableId, deck.getDeckCardLists())) {
if (SessionHandler.submitDeck(mode, tableId, deck.prepareCardsOnlyDeck())) {
SwingUtilities.invokeLater(this::removeDeckEditor);
}
return null;
@ -1617,7 +1615,7 @@ class UpdateDeckTask extends SwingWorker<Void, Void> {
@Override
protected Void doInBackground() throws Exception {
while (!isCancelled()) {
SessionHandler.updateDeck(tableId, deck.getDeckCardLists());
SessionHandler.updateDeck(tableId, deck.prepareCardsOnlyDeck());
TimeUnit.SECONDS.sleep(5);
}
return null;

View file

@ -89,7 +89,7 @@ public class DeckExportClipboardDialog extends MageDialog {
DeckExporter exporter = formats.get(formatIndex).getExporter();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
exporter.writeDeck(baos, deck.getDeckCardLists());
exporter.writeDeck(baos, deck.prepareCardsOnlyDeck());
editData.setText(baos.toString());
editData.setCaretPosition(0);
}

View file

@ -135,7 +135,7 @@ public class DeckLegalityPanel extends javax.swing.JPanel {
} else {
// contains mock cards, e.g. it's a Deck Editor
try {
deckToValidate = Deck.load(deck.getDeckCardLists(), true, false);
deckToValidate = Deck.load(deck.prepareCardsOnlyDeck(), true, false);
} catch (Exception ex) {
logger.error("Can't load real deck cards for validate: " + ex.getMessage(), ex);
return;

View file

@ -557,7 +557,7 @@ public class CustomOptionsDialog extends MageDialog {
}
if (perPlayerEmblemDeck != null) {
perPlayerEmblemDeck.clearLayouts();
options.setPerPlayerEmblemCards(perPlayerEmblemDeck.getDeckCardLists().getCards());
options.setPerPlayerEmblemCards(perPlayerEmblemDeck.prepareCardsOnlyDeck().getCards());
} else {
options.setPerPlayerEmblemCards(Collections.emptySet());
}
@ -571,7 +571,7 @@ public class CustomOptionsDialog extends MageDialog {
}
if (startingPlayerEmblemDeck != null) {
startingPlayerEmblemDeck.clearLayouts();
options.setGlobalEmblemCards(startingPlayerEmblemDeck.getDeckCardLists().getCards());
options.setGlobalEmblemCards(startingPlayerEmblemDeck.prepareCardsOnlyDeck().getCards());
} else {
options.setGlobalEmblemCards(Collections.emptySet());
}

View file

@ -394,7 +394,6 @@ public class PlayerPanelExt extends javax.swing.JPanel {
String countryName = CountryUtil.getCountryName(flagName);
basicTooltipText = "<HTML>Name: " + player.getName()
+ "<br/>Flag: " + (countryName == null ? "Unknown" : countryName)
+ "<br/>Deck hash code: " + player.getDeckHashCode()
+ "<br/>This match wins: " + player.getWins() + " of " + player.getWinsNeeded() + " (to win the match)";
}

View file

@ -1297,20 +1297,6 @@ public class SessionImpl implements Session {
return false;
}
// @Override
// public boolean startChallenge(UUID roomId, UUID tableId, UUID challengeId) {
// try {
// if (isConnected()) {
// server.startChallenge(sessionId, roomId, tableId, challengeId);
// return true;
// }
// } catch (MageException ex) {
// handleMageException(ex);
// } catch (Throwable t) {
// handleThrowable(t);
// }
// return false;
// }
@Override
public boolean submitDeck(UUID tableId, DeckCardLists deck) {
try {

View file

@ -30,7 +30,6 @@ public class PlayerView implements Serializable {
private final Counters counters;
private final int wins;
private final int winsNeeded;
private final long deckHashCode;
private final int libraryCount;
private final int handCount;
private final boolean isActive;
@ -68,8 +67,6 @@ public class PlayerView implements Serializable {
this.counters = player.getCounters();
this.wins = player.getMatchPlayer().getWins();
this.winsNeeded = player.getMatchPlayer().getWinsNeeded();
// If match ended immediately before, deck can be set to null so check is necessarry here
this.deckHashCode = player.getMatchPlayer().getDeck() != null ? player.getMatchPlayer().getDeck().getDeckHashCode() : 0;
this.libraryCount = player.getLibrary().size();
this.handCount = player.getHand().size();
this.manaPool = new ManaPoolView(player.getManaPool());
@ -206,10 +203,6 @@ public class PlayerView implements Serializable {
return winsNeeded;
}
public long getDeckHashCode() {
return deckHashCode;
}
public int getHandCount() {
return this.handCount;
}

View file

@ -54,7 +54,7 @@ public class ComputerPlayer6 extends ComputerPlayer {
private static final int MAX_SIMULATED_NODES_PER_CALC = 5000;
// same params as Executors.newFixedThreadPool
// no needs erorrs check in afterExecute here cause that pool used for FutureTask with result check already
// no needs errors check in afterExecute here cause that pool used for FutureTask with result check already
private static final ExecutorService threadPoolSimulations = new ThreadPoolExecutor(
COMPUTER_MAX_THREADS_FOR_SIMULATIONS,
COMPUTER_MAX_THREADS_FOR_SIMULATIONS,

View file

@ -13,15 +13,13 @@ public class CubeFromDeck extends DraftCube {
public CubeFromDeck(Deck cubeFromDeck) {
super("Cube From Deck");
DeckCardLists cards = null;
if (cubeFromDeck != null) {
cards = cubeFromDeck.getDeckCardLists();
if (cubeFromDeck == null) {
return;
}
if (cards != null) {
DeckCardLists cards = cubeFromDeck.prepareCardsOnlyDeck();
for (DeckCardInfo card : cards.getCards()) {
cubeCards.add(new CardIdentity(card.getCardName(), card.getSetCode(), card.getCardNum()));
}
cubeCards.add(new CardIdentity(card.getCardName(), card.getSetCode(), card.getCardNumber()));
}
}
}

View file

@ -236,35 +236,51 @@ public class TableController {
return true;
}
/**
* Check and join real player
*/
public synchronized boolean joinTable(UUID userId, String name, PlayerType playerType, int skill, DeckCardLists deckList, String password) throws MageException {
// user - must exist
Optional<User> _user = managerFactory.userManager().getUser(userId);
if (!_user.isPresent()) {
logger.error("Join Table: can't find user to join " + name + " Id = " + userId);
return false;
}
// table - already joined
User user = _user.get();
if (userPlayerMap.containsKey(userId) && playerType == PlayerType.HUMAN) {
user.showUserMessage("Join Table", new StringBuilder("You can join a table only one time.").toString());
return false;
}
// table - already started
if (table.getState() != TableState.WAITING) {
user.showUserMessage("Join Table", "No available seats.");
return false;
}
// check password
// table - wrong password
if (!table.getMatch().getOptions().getPassword().isEmpty() && playerType == PlayerType.HUMAN) {
if (!table.getMatch().getOptions().getPassword().equals(password)) {
user.showUserMessage("Join Table", "Wrong password.");
return false;
}
}
// table - no more free seats
Seat seat = table.getNextAvailableSeat(playerType);
if (seat == null) {
user.showUserMessage("Join Table", "No available seats.");
return false;
}
// deck - try to load real deck (any unknown cards will raise exception error here)
// P.S. Quick start button can raise error here on memory problems, but it's ok
// (real table creating is unaffected and will not create empty table)
Deck deck = Deck.load(deckList, false, false);
// deck - validate
if (!Main.isTestMode() && !table.getValidator().validate(deck)) {
StringBuilder sb = new StringBuilder("You (").append(name).append(") have an invalid deck for the selected ").append(table.getValidator().getName()).append(" Format. \n\n");
List<DeckValidatorError> errorsList = table.getValidator().getErrorsListSorted();
@ -273,13 +289,16 @@ public class TableController {
});
sb.append("\n\nSelect a deck that is appropriate for the selected format and try again!");
user.showUserMessage("Join Table", sb.toString());
// owner must create table with valid deck only
if (isOwner(userId)) {
logger.debug("New table removed because owner submitted invalid deck tableId " + table.getId());
logger.error("New table removed because owner submitted invalid deck tableId " + table.getId());
managerFactory.tableManager().removeTable(table.getId());
}
return false;
}
// Check quit ratio.
// user - restrict by quit ratio
int quitRatio = table.getMatch().getOptions().getQuitRatio();
if (quitRatio < user.getMatchQuitRatio()) {
String message = new StringBuilder("Your quit ratio ").append(user.getMatchQuitRatio())
@ -288,7 +307,7 @@ public class TableController {
return false;
}
// Check minimum rating.
// user - restrict by rating
int minimumRating = table.getMatch().getOptions().getMinimumRating();
int userRating;
if (table.getMatch().getOptions().isLimited()) {
@ -303,7 +322,7 @@ public class TableController {
return false;
}
// Check power level for table (currently only used for EDH/Commander table)
// user - restrict by deck power level and cards colors (see edh power level for details)
int edhPowerLevel = table.getMatch().getOptions().getEdhPowerLevel();
if (edhPowerLevel > 0 && table.getValidator().getName().toLowerCase(Locale.ENGLISH).equals("commander")) {
int deckEdhPowerLevel = table.getValidator().getEdhPowerLevel(deck);
@ -349,6 +368,7 @@ public class TableController {
}
}
// player - try to create (human, ai, etc)
Optional<Player> playerOpt = createPlayer(name, seat.getPlayerType(), skill);
if (!playerOpt.isPresent()) {
String message = "Could not create player " + name + " of type " + seat.getPlayerType();
@ -356,14 +376,18 @@ public class TableController {
user.showUserMessage("Join Table", message);
return false;
}
// player - restrict by player type, e.g. draft bot can join to tourney only
Player player = playerOpt.get();
if (!player.canJoinTable(table)) {
user.showUserMessage("Join Table", "A " + seat.getPlayerType() + " player can't join this table.");
return false;
}
// all fine, player can be added
match.addPlayer(player, deck);
table.joinTable(player, seat);
logger.trace(player.getName() + " joined tableId: " + table.getId());
//only inform human players and add them to sessionPlayerMap
if (seat.getPlayer().isHuman()) {
seat.getPlayer().setUserData(user.getUserData());
@ -373,6 +397,7 @@ public class TableController {
user.ccJoinedTable(table.getRoomId(), table.getId(), false);
userPlayerMap.put(userId, player.getId());
}
return true;
}
@ -391,8 +416,22 @@ public class TableController {
}
}
/**
* Submit deck on sideboarding/construction (final deck)
*/
public synchronized boolean submitDeck(UUID userId, DeckCardLists deckList) throws MageException {
UUID playerId = userPlayerMap.get(userId);
// player - update tourney's player status
// TODO: need research, why it here instead real time check?
if (table.isTournamentSubTable()) {
TournamentPlayer tournamentPlayer = table.getTournament().getPlayer(playerId);
if (tournamentPlayer != null) {
tournamentPlayer.setStateInfo(""); // reset sideboarding state
}
}
// player - already quit
if (table.isTournament()) {
TournamentPlayer player = tournament.getPlayer(playerId);
if (player == null || player.hasQuit()) {
@ -403,23 +442,26 @@ public class TableController {
if (mPlayer == null || mPlayer.hasQuit()) {
return true; // so the construct panel closes after submit
}
if (table.isTournamentSubTable()) {
TournamentPlayer tournamentPlayer = table.getTournament().getPlayer(mPlayer.getPlayer().getId());
if (tournamentPlayer != null) {
tournamentPlayer.setStateInfo(""); // reset sideboarding state
}
}
}
if (table.getState() != TableState.SIDEBOARDING && table.getState() != TableState.CONSTRUCTING) {
// tourney - too late for submit
if (table.getState() != TableState.SIDEBOARDING
&& table.getState() != TableState.CONSTRUCTING) {
return false;
}
// deck - try to load real deck (any unknown cards will raise exception error here)
Deck deck = Deck.load(deckList, false, false);
// workaround to keep deck name for Tiny Leaders because it must be hidden for players
if (table.getState() == TableState.SIDEBOARDING && table.getMatch() != null) {
MatchPlayer mPlayer = table.getMatch().getPlayer(playerId);
if (mPlayer != null) {
deck.setName(mPlayer.getDeck().getName());
}
}
// deck - validate
if (!Main.isTestMode() && !table.getValidator().validate(deck)) {
Optional<User> _user = managerFactory.userManager().getUser(userId);
if (!_user.isPresent()) {
@ -434,21 +476,20 @@ public class TableController {
_user.get().showUserMessage("Submit deck", sb.toString());
return false;
}
submitDeck(userId, playerId, deck);
return true;
}
public void updateDeck(UUID userId, DeckCardLists deckList) throws MageException {
boolean validDeck;
UUID playerId = userPlayerMap.get(userId);
if (table.getState() != TableState.SIDEBOARDING && table.getState() != TableState.CONSTRUCTING) {
// TODO: need refactor, many duplicated code like state check
if (table.getState() != TableState.SIDEBOARDING
&& table.getState() != TableState.CONSTRUCTING) {
return;
}
Deck deck = Deck.load(deckList, false, false);
validDeck = updateDeck(userId, playerId, deck);
if (!validDeck && getTableState() == TableState.SIDEBOARDING) {
logger.warn(" userId: " + userId + " - Modified deck card list!");
}
updateDeck(userId, playerId, deck);
}
private void submitDeck(UUID userId, UUID playerId, Deck deck) {
@ -461,20 +502,21 @@ public class TableController {
}
}
private boolean updateDeck(UUID userId, UUID playerId, Deck deck) {
boolean validDeck = true;
private void updateDeck(UUID userId, UUID playerId, Deck deck) {
if (table.isTournament()) {
if (tournament != null) {
validDeck = managerFactory.tournamentManager().updateDeck(tournament.getId(), playerId, deck);
// TODO: is it possible to update from direct call command in game?!
managerFactory.tournamentManager().updateDeck(tournament.getId(), playerId, deck);
} else {
logger.fatal("Tournament == null table: " + table.getId() + " userId: " + userId);
}
} else if (TableState.SIDEBOARDING == table.getState()) {
validDeck = match.updateDeck(playerId, deck);
} else if (table.getState() == TableState.SIDEBOARDING) {
match.updateDeck(playerId, deck);
} else {
// deck was meanwhile submitted so the autoupdate can be ignored
// TODO: need research
logger.warn("wtf, why it submitting?!");
}
return validDeck;
}
public boolean watchTable(UUID userId) {
@ -538,6 +580,7 @@ public class TableController {
} else {
UUID playerId = userPlayerMap.get(userId);
if (playerId != null) {
// TODO: wtf, need research and document all that checks, can be never used code
if (table.getState() == TableState.WAITING || table.getState() == TableState.READY_TO_START) {
table.leaveNotStartedTable(playerId);
if (table.isTournament()) {
@ -884,7 +927,7 @@ public class TableController {
private void autoSideboard() {
for (MatchPlayer player : match.getPlayers()) {
if (!player.isDoneSideboarding()) {
match.submitDeck(player.getPlayer().getId(), player.generateDeck(table.getValidator()));
match.submitDeck(player.getPlayer().getId(), player.autoCompleteDeck(table.getValidator()));
}
}
}

View file

@ -22,7 +22,7 @@ public interface TournamentManager {
void submitDeck(UUID tournamentId, UUID playerId, Deck deck);
boolean updateDeck(UUID tournamentId, UUID playerId, Deck deck);
void updateDeck(UUID tournamentId, UUID playerId, Deck deck);
TournamentView getTournamentView(UUID tournamentId);

View file

@ -324,11 +324,13 @@ public class TournamentController {
}
}
public boolean updateDeck(UUID playerId, Deck deck) {
if (tournamentSessions.containsKey(playerId)) {
return tournamentSessions.get(playerId).updateDeck(deck);
public void updateDeck(UUID playerId, Deck deck) {
TournamentSession session = tournamentSessions.getOrDefault(playerId, null);
if (session == null) {
return;
}
return false;
session.updateDeck(deck);
}
public void timeout(UUID userId) {

View file

@ -17,6 +17,7 @@ import java.util.concurrent.ConcurrentMap;
*/
public class TournamentManagerImpl implements TournamentManager {
// TODO: must check all usage of controllers.get and insert null check and error log
private final ManagerFactory managerFactory;
private final ConcurrentMap<UUID, TournamentController> controllers = new ConcurrentHashMap<>();
@ -61,8 +62,8 @@ public class TournamentManagerImpl implements TournamentManager {
}
@Override
public boolean updateDeck(UUID tournamentId, UUID playerId, Deck deck) {
return controllers.get(tournamentId).updateDeck(playerId, deck);
public void updateDeck(UUID tournamentId, UUID playerId, Deck deck) {
controllers.get(tournamentId).updateDeck(playerId, deck);
}
@Override

View file

@ -83,8 +83,8 @@ public class TournamentSession {
tournament.submitDeck(playerId, deck);
}
public boolean updateDeck(Deck deck) {
return tournament.updateDeck(playerId, deck);
public void updateDeck(Deck deck) {
tournament.updateDeck(playerId, deck);
}
public void setKilled() {

View file

@ -0,0 +1,241 @@
package org.mage.test.serverside.deck;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckCardInfo;
import mage.cards.decks.DeckCardLayout;
import mage.cards.decks.DeckCardLists;
import mage.game.GameException;
import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.MageTestPlayerBase;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author JayDi85
*/
public class DeckHashTest extends MageTestPlayerBase {
private static final DeckCardInfo BAD_CARD = new DeckCardInfo("unknown card", "123", "xxx");
private static final DeckCardInfo GOOD_CARD_1 = new DeckCardInfo("Lightning Bolt", "141", "CLU");
private static final DeckCardInfo GOOD_CARD_1_ANOTHER_SET = new DeckCardInfo("Lightning Bolt", "146", "M10");
private static final DeckCardInfo GOOD_CARD_1_x3 = new DeckCardInfo("Lightning Bolt", "141", "CLU", 3);
private static final DeckCardInfo GOOD_CARD_2 = new DeckCardInfo("Bear's Companion", "182", "2x2");
private static final DeckCardInfo GOOD_CARD_3 = new DeckCardInfo("Amplifire", "92", "RNA");
private void assertDecks(String check, boolean mustBeSame, Deck deck1, Deck deck2) {
Assert.assertEquals(
check + " - " + (mustBeSame ? "hash code must be same" : "hash code must be different"),
mustBeSame,
deck1.getDeckHash() == deck2.getDeckHash()
);
}
private Deck prepareCardsDeck(List<DeckCardInfo> mainCards, List<DeckCardInfo> sideboardCards) {
DeckCardLists deckSource = new DeckCardLists();
deckSource.getCards().addAll(mainCards);
deckSource.getSideboard().addAll(sideboardCards);
try {
return Deck.load(deckSource, true);
} catch (GameException e) {
throw new IllegalArgumentException("Must not catch exception, but found " + e, e);
} catch (Throwable e) {
Assert.fail("wtf");
}
return null;
}
private Deck prepareEmptyDeck(String name, String author, boolean addMainLayout, boolean addSideboardLayout) {
DeckCardLists deckSource = new DeckCardLists();
deckSource.setName(name);
deckSource.setAuthor(author);
if (addMainLayout) {
deckSource.setCardLayout(new DeckCardLayout(new ArrayList<>(), "xxx"));
}
if (addSideboardLayout) {
deckSource.setCardLayout(new DeckCardLayout(new ArrayList<>(), "xxx"));
}
try {
return Deck.load(deckSource, true);
} catch (GameException e) {
throw new IllegalArgumentException("Must not catch exception, but found " + e, e);
}
}
@Test
public void test_MustIgnoreNonCardsData() {
// additional info must be ignored for deck hash
Deck emptyDeck = new Deck();
assertDecks("empty with empty", true, emptyDeck, emptyDeck);
assertDecks("empty with new empty", true, emptyDeck, new Deck());
assertDecks("empty with name", true, emptyDeck, prepareEmptyDeck("name", "", false, false));
assertDecks("empty with author", true, emptyDeck, prepareEmptyDeck("", "author", false, false));
assertDecks("empty with layout main", true, emptyDeck, prepareEmptyDeck("", "", true, false));
assertDecks("empty with layout sideboard", true, emptyDeck, prepareEmptyDeck("", "", false, true));
assertDecks("empty with all", true, emptyDeck, prepareEmptyDeck("name", "author", true, true));
assertDecks("empty with non empty", false, emptyDeck, prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()));
}
@Test
public void test_MustUseCardNameAndAmountOnly() {
// card names and amount must be important
// set and card number must be ignored
assertDecks(
"empty",
true,
prepareCardsDeck(Arrays.asList(), Arrays.asList()),
prepareCardsDeck(Arrays.asList(), Arrays.asList())
);
assertDecks(
"same names",
true,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList())
);
assertDecks(
"same names but diff sets",
true,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1_ANOTHER_SET), Arrays.asList())
);
assertDecks(
"diff names",
false,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_2), Arrays.asList())
);
assertDecks(
"same main, same side",
true,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3)),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3))
);
assertDecks(
"diff main, same side",
false,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3)),
prepareCardsDeck(Arrays.asList(GOOD_CARD_2), Arrays.asList(GOOD_CARD_3))
);
assertDecks(
"same main, diff side",
false,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_2)),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList(GOOD_CARD_3))
);
assertDecks(
"same names, but diff amount - main",
false,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1_x3), Arrays.asList())
);
assertDecks(
"same names, but diff amount - side",
false,
prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1)),
prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1_x3))
);
}
@Test
public void test_MustIgnoreCardsOrder() {
// deck hash must use sorted list, so deck order must be ignored
assertDecks(
"diff order - main",
true,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1, GOOD_CARD_1, GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1_x3), Arrays.asList())
);
assertDecks(
"diff order - side",
true,
prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1, GOOD_CARD_1, GOOD_CARD_1)),
prepareCardsDeck(Arrays.asList(), Arrays.asList(GOOD_CARD_1_x3))
);
assertDecks(
"diff order - diff names",
true,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1, GOOD_CARD_2, GOOD_CARD_3), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_3, GOOD_CARD_1, GOOD_CARD_2), Arrays.asList())
);
}
@Test
public void test_MustIgnoreUnknownCards() {
// hash must use only real cards and ignore missing
assertDecks(
"empty and unknown",
true,
prepareCardsDeck(Arrays.asList(), Arrays.asList()),
prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList())
);
assertDecks(
"unknown and unknown",
true,
prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()),
prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList())
);
assertDecks(
"unknown and x2 unknown",
true,
prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList()),
prepareCardsDeck(Arrays.asList(BAD_CARD, BAD_CARD), Arrays.asList())
);
assertDecks(
"card and unknown",
false,
prepareCardsDeck(Arrays.asList(GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList())
);
assertDecks(
"card + unknown and unknown",
false,
prepareCardsDeck(Arrays.asList(BAD_CARD, GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(BAD_CARD), Arrays.asList())
);
assertDecks(
"card + unknown and card + unknown",
true,
prepareCardsDeck(Arrays.asList(BAD_CARD, GOOD_CARD_1), Arrays.asList()),
prepareCardsDeck(Arrays.asList(GOOD_CARD_1, BAD_CARD), Arrays.asList())
);
}
@Test
public void test_MustRaiseErrorOnTooManyCards() {
// good amount
Assert.assertEquals(90, new DeckCardInfo(GOOD_CARD_1.getCardName(), GOOD_CARD_1.getCardNumber(), GOOD_CARD_1.getSetCode(), 90).getAmount());
// bad amount
try {
new DeckCardInfo(GOOD_CARD_1.getCardName(), GOOD_CARD_1.getCardNumber(), GOOD_CARD_1.getSetCode(), 100);
} catch (Exception e) {
if (e.getMessage().contains("Found too big amount")) {
// good
return;
}
}
Assert.fail("must raise exception on too big amount");
}
}

View file

@ -85,8 +85,8 @@ public class TournamentStub implements Tournament {
}
@Override
public boolean updateDeck(UUID playerId, Deck deck) {
return true;
public void updateDeck(UUID playerId, Deck deck) {
}
@Override

View file

@ -1,28 +1,34 @@
package mage.cards.decks;
import mage.MageObject;
import mage.cards.Card;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.game.GameException;
import mage.util.Copyable;
import mage.util.DeckUtil;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.*;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Server side deck with workable cards, also can be used in GUI
* <p>
* If you need text only deck then look at DeckCardLists
*/
public class Deck implements Serializable, Copyable<Deck> {
static final int MAX_CARDS_PER_DECK = 2000;
private String name;
private String name; // TODO: must rework somehow - tiny leaders use deck name to find commander card and hide it for a user
private final Set<Card> cards = new LinkedHashSet<>();
private final Set<Card> sideboard = new LinkedHashSet<>();
private DeckCardLayout cardsLayout;
private DeckCardLayout sideboardLayout;
private long deckHashCode = 0;
private long deckCompleteHashCode = 0;
private DeckCardLayout cardsLayout; // client side only
private DeckCardLayout sideboardLayout; // client side only
public Deck() {
super();
@ -34,8 +40,6 @@ public class Deck implements Serializable, Copyable<Deck> {
this.sideboard.addAll(deck.sideboard.stream().map(Card::copy).collect(Collectors.toList()));
this.cardsLayout = deck.cardsLayout == null ? null : deck.cardsLayout.copy();
this.sideboardLayout = deck.sideboardLayout == null ? null : deck.sideboardLayout.copy();
this.deckHashCode = deck.deckHashCode;
this.deckCompleteHashCode = deck.deckCompleteHashCode;
}
public static Deck load(DeckCardLists deckCardLists) throws GameException {
@ -46,27 +50,15 @@ public class Deck implements Serializable, Copyable<Deck> {
return Deck.load(deckCardLists, ignoreErrors, true);
}
public static Deck append(Deck deckToAppend, Deck currentDeck) throws GameException {
List<String> deckCardNames = new ArrayList<>();
for (Card card : deckToAppend.getCards()) {
if (card != null) {
currentDeck.cards.add(card);
deckCardNames.add(card.getName());
}
}
List<String> sbCardNames = new ArrayList<>();
for (Card card : deckToAppend.getSideboard()) {
if (card != null) {
currentDeck.sideboard.add(card);
deckCardNames.add(card.getName());
}
}
Collections.sort(deckCardNames);
Collections.sort(sbCardNames);
String deckString = deckCardNames.toString() + sbCardNames.toString();
currentDeck.setDeckHashCode(DeckUtil.fixedHash(deckString));
return currentDeck;
public static Deck append(Deck sourceDeck, Deck currentDeck) throws GameException {
Deck newDeck = currentDeck.copy();
sourceDeck.getCards().forEach(card -> {
newDeck.cards.add(card.copy());
});
sourceDeck.getSideboard().forEach(card -> {
newDeck.sideboard.add(card.copy());
});
return newDeck;
}
public static Deck load(DeckCardLists deckCardLists, boolean ignoreErrors, boolean mockCards) throws GameException {
@ -87,49 +79,45 @@ public class Deck implements Serializable, Copyable<Deck> {
deck.setName(deckCardLists.getName());
deck.cardsLayout = deckCardLists.getCardLayout() == null ? null : deckCardLists.getCardLayout().copy();
deck.sideboardLayout = deckCardLists.getSideboardLayout() == null ? null : deckCardLists.getSideboardLayout().copy();
List<String> deckCardNames = new ArrayList<>();
int totalCards = 0;
for (DeckCardInfo deckCardInfo : deckCardLists.getCards()) {
Card card = createCard(deckCardInfo, mockCards, cardInfoCache);
if (card != null) {
if (totalCards > MAX_CARDS_PER_DECK) {
break;
}
deck.cards.add(card);
deckCardNames.add(card.getName());
totalCards++;
} else if (!ignoreErrors) {
throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName());
// load only real cards
int totalCards = 0;
// main
main:
for (DeckCardInfo deckCardInfo : deckCardLists.getCards()) {
for (int i = 1; i <= deckCardInfo.getAmount(); i++) {
if (totalCards > MAX_CARDS_PER_DECK) {
break main;
}
}
List<String> sbCardNames = new ArrayList<>();
for (DeckCardInfo deckCardInfo : deckCardLists.getSideboard()) {
Card card = createCard(deckCardInfo, mockCards, cardInfoCache);
if (card != null) {
if (totalCards > MAX_CARDS_PER_DECK) {
break;
}
deck.sideboard.add(card);
sbCardNames.add(card.getName());
deck.cards.add(card);
totalCards++;
} else if (!ignoreErrors) {
throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName());
}
}
Collections.sort(deckCardNames);
Collections.sort(sbCardNames);
String deckString = deckCardNames.toString() + sbCardNames.toString();
deck.setDeckHashCode(DeckUtil.fixedHash(deckString));
if (sbCardNames.isEmpty()) {
deck.setDeckCompleteHashCode(deck.getDeckHashCode());
} else {
List<String> deckAllCardNames = new ArrayList<>();
deckAllCardNames.addAll(deckCardNames);
deckAllCardNames.addAll(sbCardNames);
Collections.sort(deckAllCardNames);
deck.setDeckCompleteHashCode(DeckUtil.fixedHash(deckAllCardNames.toString()));
}
// sideboard
side:
for (DeckCardInfo deckCardInfo : deckCardLists.getSideboard()) {
for (int i = 1; i <= deckCardInfo.getAmount(); i++) {
if (totalCards > MAX_CARDS_PER_DECK) {
break side;
}
Card card = createCard(deckCardInfo, mockCards, cardInfoCache);
if (card != null) {
deck.sideboard.add(card);
totalCards++;
} else if (!ignoreErrors) {
throw createCardNotFoundGameException(deckCardInfo, deckCardLists.getName());
}
}
}
return deck;
}
@ -149,7 +137,7 @@ public class Deck implements Serializable, Copyable<Deck> {
String cardError = String.format("Card not found - %s - %s - %s in deck %s.",
deckCardInfo.getCardName(),
deckCardInfo.getSetCode(),
deckCardInfo.getCardNum(),
deckCardInfo.getCardNumber(),
deckName
);
cardError += "\n\nPossible reasons:";
@ -168,15 +156,15 @@ public class Deck implements Serializable, Copyable<Deck> {
CardInfo cardInfo;
if (cardInfoCache != null) {
// from cache
String key = String.format("%s_%s", deckCardInfo.getSetCode(), deckCardInfo.getCardNum());
String key = String.format("%s_%s", deckCardInfo.getSetCode(), deckCardInfo.getCardNumber());
cardInfo = cardInfoCache.getOrDefault(key, null);
if (cardInfo == null) {
cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum());
cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNumber());
cardInfoCache.put(key, cardInfo);
}
} else {
// from db
cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNum());
cardInfo = CardRepository.instance.findCard(deckCardInfo.getSetCode(), deckCardInfo.getCardNumber());
}
if (cardInfo == null) {
@ -190,13 +178,18 @@ public class Deck implements Serializable, Copyable<Deck> {
}
}
public DeckCardLists getDeckCardLists() {
/**
* Prepare new deck with cards only without layout and other client side settings.
* Use it for any export and server's deck update/submit
*/
public DeckCardLists prepareCardsOnlyDeck() {
DeckCardLists deckCardLists = new DeckCardLists();
deckCardLists.setName(name);
for (Card card : cards) {
deckCardLists.getCards().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getExpansionSetCode()));
}
for (Card card : sideboard) {
deckCardLists.getSideboard().add(new DeckCardInfo(card.getName(), card.getCardNumber(), card.getExpansionSetCode()));
}
@ -227,6 +220,7 @@ public class Deck implements Serializable, Copyable<Deck> {
return cards;
}
// TODO: delete and replace by getCards()
public Set<Card> getMaindeckCards() {
return cards
.stream()
@ -262,22 +256,6 @@ public class Deck implements Serializable, Copyable<Deck> {
return sideboardLayout;
}
public long getDeckHashCode() {
return deckHashCode;
}
public void setDeckHashCode(long deckHashCode) {
this.deckHashCode = deckHashCode;
}
public long getDeckCompleteHashCode() {
return deckCompleteHashCode;
}
public void setDeckCompleteHashCode(long deckHashCode) {
this.deckCompleteHashCode = deckHashCode;
}
public void clearLayouts() {
this.cardsLayout = null;
this.sideboardLayout = null;
@ -287,4 +265,11 @@ public class Deck implements Serializable, Copyable<Deck> {
public Deck copy() {
return new Deck(this);
}
public long getDeckHash() {
return DeckUtil.getDeckHash(
this.cards.stream().map(MageObject::getName).collect(Collectors.toList()),
this.sideboard.stream().map(MageObject::getName).collect(Collectors.toList())
);
}
}

View file

@ -1,5 +1,3 @@
package mage.cards.decks;
import mage.util.Copyable;
@ -7,14 +5,18 @@ import mage.util.Copyable;
import java.io.Serializable;
/**
* Client side: card record in deck file
*
* @author LevelX2
*/
public class DeckCardInfo implements Serializable, Copyable<DeckCardInfo> {
static final int MAX_AMOUNT_PER_CARD = 99;
private String cardName;
private String setCode;
private String cardNum;
private int quantity;
private String cardNumber;
private int amount;
public DeckCardInfo() {
super();
@ -23,19 +25,29 @@ public class DeckCardInfo implements Serializable, Copyable<DeckCardInfo> {
protected DeckCardInfo(final DeckCardInfo info) {
this.cardName = info.cardName;
this.setCode = info.setCode;
this.cardNum = info.cardNum;
this.quantity = info.quantity;
this.cardNumber = info.cardNumber;
this.amount = info.amount;
}
public DeckCardInfo(String cardName, String cardNum, String setCode) {
this(cardName, cardNum, setCode, 1);
public DeckCardInfo(String cardName, String cardNumber, String setCode) {
this(cardName, cardNumber, setCode, 1);
}
public DeckCardInfo(String cardName, String cardNum, String setCode, int quantity) {
public static void makeSureCardAmountFine(int amount, String cardName) {
// runtime check
if (amount > MAX_AMOUNT_PER_CARD) {
// xmage uses 1 for amount all around, but keep that protection anyway
throw new IllegalArgumentException("Found too big amount for a deck's card: " + cardName + " - " + amount);
}
}
public DeckCardInfo(String cardName, String cardNumber, String setCode, int amount) {
makeSureCardAmountFine(amount, cardName);
this.cardName = cardName;
this.cardNum = cardNum;
this.cardNumber = cardNumber;
this.setCode = setCode;
this.quantity = quantity;
this.amount = amount;
}
public String getCardName() {
@ -46,21 +58,16 @@ public class DeckCardInfo implements Serializable, Copyable<DeckCardInfo> {
return setCode;
}
public String getCardNum() {
return cardNum;
public String getCardNumber() {
return cardNumber;
}
public int getQuantity() {
return quantity;
}
public DeckCardInfo increaseQuantity() {
quantity++;
return this;
public int getAmount() {
return amount;
}
public String getCardKey() {
return setCode + cardNum;
return setCode + cardNumber;
}
@Override

View file

@ -6,9 +6,11 @@ import mage.util.Copyable;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Client side deck with text only.
* <p>
* Can contain restricted, un-implemented or unknown cards
*
* @author BetaSteward_at_googlemail.com, JayDi85
*/
@ -46,12 +48,15 @@ public class DeckCardLists implements Serializable, Copyable<DeckCardLists> {
public DeckCardLayout getCardLayout() {
return cardLayout;
}
public void setCardLayout(DeckCardLayout layout) {
this.cardLayout = layout;
}
public DeckCardLayout getSideboardLayout() {
return sideboardLayout;
}
public void setSideboardLayout(DeckCardLayout layout) {
this.sideboardLayout = layout;
}

View file

@ -72,6 +72,8 @@ public enum DeckFormats {
return res;
}
// TODO: need refactor and remove all that methods to 1-3 versions only
public static void writeDeck(String file, DeckCardLists deck) throws IOException {
writeDeck(new File(file), deck);
}

View file

@ -56,8 +56,9 @@ public enum ExpansionRepository {
instanceInitialized = true;
eventSource.fireRepositoryDbLoaded();
} catch (SQLException ex) {
ex.printStackTrace();
} catch (SQLException e) {
// TODO: add app close?
e.printStackTrace();
}
}

View file

@ -11,7 +11,6 @@ import mage.cards.repository.CardRepository;
import mage.constants.Zone;
import mage.game.command.Emblem;
import mage.util.CardUtil;
import org.apache.log4j.Logger;
import java.util.List;
import java.util.stream.Collectors;
@ -50,7 +49,7 @@ public final class EmblemOfCard extends Emblem {
public static Card cardFromDeckInfo(DeckCardInfo info) {
return lookupCard(
info.getCardName(),
info.getCardNum(),
info.getCardNumber(),
info.getSetCode(),
"DeckCardInfo"
);

View file

@ -41,7 +41,7 @@ public interface Match {
void submitDeck(UUID playerId, Deck deck);
boolean updateDeck(UUID playerId, Deck deck);
void updateDeck(UUID playerId, Deck deck);
void startMatch();

View file

@ -379,9 +379,8 @@ public abstract class MatchImpl implements Match {
public void submitDeck(UUID playerId, Deck deck) {
MatchPlayer player = getPlayer(playerId);
if (player != null) {
// make sure the deck name (needed for Tiny Leaders) won't get lost by sideboarding
// workaround to keep deck name for Tiny Leaders because it must be hidden for players
deck.setName(player.getDeck().getName());
deck.setDeckHashCode(player.getDeck().getDeckHashCode());
player.submitDeck(deck);
}
synchronized (this) {
@ -390,21 +389,15 @@ public abstract class MatchImpl implements Match {
}
@Override
public boolean updateDeck(UUID playerId, Deck deck) {
boolean validDeck = true;
public void updateDeck(UUID playerId, Deck deck) {
// used for auto-save deck
MatchPlayer player = getPlayer(playerId);
if (player != null) {
// Check if the cards included in the deck are the same as in the original deck
validDeck = (player.getDeck().getDeckCompleteHashCode() == deck.getDeckCompleteHashCode());
if (validDeck == false) {
// clear the deck so the player cheating looses the game
deck.getCards().clear();
deck.getSideboard().clear();
if (player == null) {
return;
}
player.updateDeck(deck);
}
return validDeck;
}
protected String createGameStartMessage() {
StringBuilder sb = new StringBuilder();
@ -416,9 +409,6 @@ public abstract class MatchImpl implements Match {
sb.append(" QUITTED");
}
sb.append("<br/>");
if (mp.getDeck() != null) {
sb.append("DeckHash: ").append(mp.getDeck().getDeckHashCode()).append("<br/>");
}
}
if (getDraws() > 0) {
sb.append(" Draws: ").append(getDraws()).append("<br/>");

View file

@ -4,6 +4,7 @@ import mage.cards.Card;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckValidator;
import mage.players.Player;
import org.apache.log4j.Logger;
import java.io.Serializable;
@ -12,13 +13,15 @@ import java.io.Serializable;
*/
public class MatchPlayer implements Serializable {
private static final Logger logger = Logger.getLogger(MatchPlayer.class);
private int wins;
private int winsNeeded;
private boolean matchWinner;
private Deck deck;
private Player player;
private String name;
private final Player player;
private final String name;
private boolean quit;
private boolean doneSideboarding;
@ -89,22 +92,33 @@ public class MatchPlayer implements Serializable {
return viewerDeck;
}
public void submitDeck(Deck deck) {
this.deck = deck;
public void submitDeck(Deck newDeck) {
this.deck = newDeck;
this.doneSideboarding = true;
}
public void updateDeck(Deck deck) {
public boolean updateDeck(Deck newDeck) {
// used for auto-save by timeout from client side
// workaround to keep deck name for Tiny Leaders because it must be hidden for players
if (this.deck != null) {
// preserver deck name, important for Tiny Leaders format
deck.setName(this.getDeck().getName());
// preserve the original deck hash code before sideboarding to give no information if cards were swapped
deck.setDeckHashCode(this.getDeck().getDeckHashCode());
}
this.deck = deck;
newDeck.setName(this.getDeck().getName());
}
public Deck generateDeck(DeckValidator deckValidator) {
// make sure it's the same deck (player do not add or remove something)
boolean isGood = (this.deck.getDeckHash() == newDeck.getDeckHash());
if (!isGood) {
logger.error("Found cheating player " + player.getName()
+ " with changed deck, main " + newDeck.getCards().size() + ", side " + newDeck.getSideboard().size());
newDeck.getCards().clear();
newDeck.getSideboard().clear();
}
this.deck = newDeck;
return isGood;
}
public Deck autoCompleteDeck(DeckValidator deckValidator) {
// auto complete deck
while (deck.getMaindeckCards().size() < deckValidator.getDeckMinSize() && !deck.getSideboard().isEmpty()) {
Card card = deck.getSideboard().iterator().next();

View file

@ -49,7 +49,7 @@ public interface Tournament {
void submitDeck(UUID playerId, Deck deck);
boolean updateDeck(UUID playerId, Deck deck);
void updateDeck(UUID playerId, Deck deck);
void autoSubmit(UUID playerId, Deck deck);

View file

@ -139,11 +139,13 @@ public abstract class TournamentImpl implements Tournament {
}
@Override
public boolean updateDeck(UUID playerId, Deck deck) {
if (players.containsKey(playerId)) {
return players.get(playerId).updateDeck(deck);
public void updateDeck(UUID playerId, Deck deck) {
TournamentPlayer player = players.getOrDefault(playerId, null);
if (player == null) {
return;
}
return false;
player.updateDeck(deck);
}
protected Round createRoundRandom() {

View file

@ -93,18 +93,18 @@ public class TournamentPlayer {
}
public boolean updateDeck(Deck deck) {
// Check if the cards included in the deck are the same as in the original deck
boolean validDeck = (getDeck().getDeckCompleteHashCode() == deck.getDeckCompleteHashCode());
if (!validDeck) {
// Clear the deck so the player cheating looses the game
// TODO: inform other players about cheating?!
logger.error("Found cheating player " + getPlayer().getName()
// used for auto-save deck
// make sure it's the same deck (player do not add or remove something)
boolean isGood = (this.getDeck().getDeckHash() == deck.getDeckHash());
if (!isGood) {
logger.error("Found cheating tourney player " + player.getName()
+ " with changed deck, main " + deck.getCards().size() + ", side " + deck.getSideboard().size());
deck.getCards().clear();
deck.getSideboard().clear();
}
this.deck = deck;
return validDeck;
return isGood;
}
/**

View file

@ -1,20 +1,36 @@
package mage.util;
import mage.cards.decks.Deck;
import mage.players.Player;
import org.apache.log4j.Logger;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author LevelX2
* @author LevelX2, JayDi85
*/
public final class DeckUtil {
private static final Logger logger = Logger.getLogger(DeckUtil.class);
public static long fixedHash(String string) {
/**
* Find deck hash (main + sideboard)
* It must be same after sideboard (do not depend on main/sb structure)
*/
public static long getDeckHash(List<String> mainCards, List<String> sideboardCards) {
List<String> all = new ArrayList<>(mainCards);
all.addAll(sideboardCards);
Collections.sort(all);
return getStringHash(all.toString());
}
private static long getStringHash(String string) {
long h = 1125899906842597L; // prime
int len = string.length();
@ -42,4 +58,27 @@ public final class DeckUtil {
}
return null;
}
/**
* make sure it's the same deck (player do not add or remove something)
*
* @param newDeck will be clear on cheating
*/
public boolean checkDeckForModification(String checkInfo, Player player, Deck oldDeck, Deck newDeck) {
boolean isGood = (oldDeck.getDeckHash() == newDeck.getDeckHash());
if (!isGood) {
String message = String.format("Found cheating player [%s] in [%s] with changed deck, main %d -> %d, side %d -> %d",
player.getName(),
checkInfo,
oldDeck.getCards().size(),
newDeck.getCards().size(),
oldDeck.getSideboard().size(),
newDeck.getSideboard().size()
);
logger.error(message);
newDeck.getCards().clear();
newDeck.getSideboard().clear();
}
return isGood;
}
}