diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 000000000..715d6b3ba --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: core.LLMTest + diff --git a/data/cantstop/ruleSummary.txt b/data/cantstop/ruleSummary.txt new file mode 100644 index 000000000..7ca13f61a --- /dev/null +++ b/data/cantstop/ruleSummary.txt @@ -0,0 +1,31 @@ +**Game Implementation Summary** + +**Game Rules:** + +1. **Dice Rolling:** Players roll four dice, split into two pairs to determine column movements. +2. **Column Movement:** Each pair dictates movement in one column. +3. **Turn End:** Players can stop rolling and end their turn at any time to secure progress. +4. **Winning Columns:** Players win a column by reaching the top with a marker. +5. **Loss Condition:** Rolling higher numbers after winning a column can cause loss of all placed markers. +6. **Marker Placement:** Markers cannot be placed in already won columns. +7. **Game Win:** The first player to claim any three columns wins. + +**Strategy:** + +1. **Focus Columns:** Prioritize columns with numbers 6, 7, or 8 due to higher probability. +2. **Avoid Short Columns:** Columns with fewer steps are less advantageous. +3. **Risk Management:** Stop rolling to secure progress and avoid losing all markers in a column. +4. **Marker Interaction:** If a player's marker lands on another's, they must roll again until moving or losing their turn (blowing it). + +**Game Variant:** + +- **Four Players:** Encourages more risk-taking due to increased competition. + +**Developer Notes:** + +- Implement dice rolling mechanics with split pair results. +- Include rules for column movement and marker placement. +- Create logic for winning and losing conditions. +- Add a focus on strategy with column prioritization. +- Integrate the interaction rule for markers on the same space. +- Consider enhancing the game with a four-player mode for increased challenge. \ No newline at end of file diff --git a/data/cantstop/rulebook.pdf b/data/cantstop/rulebook.pdf new file mode 100644 index 000000000..93483300b Binary files /dev/null and b/data/cantstop/rulebook.pdf differ diff --git a/data/coltexpress/rulebook.pdf b/data/coltexpress/rulebook.pdf new file mode 100644 index 000000000..16037003e Binary files /dev/null and b/data/coltexpress/rulebook.pdf differ diff --git a/data/connect4/ruleSummary.txt b/data/connect4/ruleSummary.txt new file mode 100644 index 000000000..a101311d1 --- /dev/null +++ b/data/connect4/ruleSummary.txt @@ -0,0 +1,23 @@ +**"Connect Four" Game Rules and Strategy Summary:** + +- **Objective:** Align four checkers of your color vertically, horizontally, or diagonally on a 7x6 grid. +- **Players:** Two players, taking turns. +- **Gameplay:** + - Players take turns dropping checkers into the top of the grid. + - Checkers fall to the lowest available row. + - The game ends when a player achieves a winning combination or the grid is full. +- **Winning Condition:** First player to align four checkers in a row (vertical, horizontal, or diagonal) wins. +- **Post-Game:** Winning player slides a lever to clear the grid, preparing for the next game. + +**Strategy:** +- **Center Control:** Aim to control the center columns (3 and 4) to have the most line options. +- **Block Opponent:** Prevent the opponent from getting three in a row, especially in the center. +- **Forks:** Create opportunities where you can win in multiple lines in your next turn. +- **Force Opponent:** Force the opponent into positions where they must block you, limiting their own opportunities. +- **Corner Play:** Use corners strategically, especially for diagonals. + +**Development Notes:** +- Implement a 7x6 grid. +- Check for winning combinations after each turn. +- Handle full grid (no more moves) scenario. +- Include lever mechanism to clear the grid after a win. \ No newline at end of file diff --git a/data/connect4/rulebook.pdf b/data/connect4/rulebook.pdf new file mode 100644 index 000000000..348c328b6 Binary files /dev/null and b/data/connect4/rulebook.pdf differ diff --git a/data/diamant/ruleSummary.txt b/data/diamant/ruleSummary.txt new file mode 100644 index 000000000..f2150af40 --- /dev/null +++ b/data/diamant/ruleSummary.txt @@ -0,0 +1,32 @@ +**Game Summary for Implementation** + +**Game Overview:** +- 3-8 players, aged 8+ +- 30-minute playtime +- Players explore a cave, collect diamonds and rubies, avoid traps +- Goal: gather the most diamonds by the end of the game + +**Game Components:** +- Board: 5 paths connecting camp to the cave +- Cards: Decision, Treasure, Trap +- Barricade tiles: track progress +- Chests: store rubies and diamonds + +**Game Rules:** +1. **Treasure Collection:** Players gather rubies from Treasure cards, sharing equally and keeping extras. +2. **Traps:** Two Trap cards end the expedition. +3. **Movement:** Players simultaneously reveal cards, gather treasure, and decide to continue or exit. +4. **Exiting:** Exiting players share rubies, and the first to leave takes Relics for Diamonds. +5. **Game End:** When all players return to camp or two Traps appear. +6. **Rounds:** After each round, place a Barricade tile, reallocate Rubies and Relics, reshuffle the Expedition deck. + +**Strategy:** +1. **Treasure Collection:** Continue the expedition to gather more treasure but be cautious of Trap cards. +2. **Timing Exits:** Decide to exit when you have enough rubies to secure your score. +3. **Leveraging Diamonds:** Maximize treasure collection and use diamonds for victory points. + +**Key Mechanics:** +- Simultaneous card revelation +- Resource sharing and management +- Risk-reward decision-making +- Progress tracking with Barricade tiles \ No newline at end of file diff --git a/data/diamant/rulebook.pdf b/data/diamant/rulebook.pdf new file mode 100644 index 000000000..47a1dd2c8 Binary files /dev/null and b/data/diamant/rulebook.pdf differ diff --git a/data/dominion/ruleSummary.txt b/data/dominion/ruleSummary.txt new file mode 100644 index 000000000..1f65136d7 --- /dev/null +++ b/data/dominion/ruleSummary.txt @@ -0,0 +1,44 @@ +**Game Summary for Implementation** + +**Game Rules:** +- Players build a deck (Dominion) using Treasure, Action, and Victory cards to gain victory points. +- Players start with 7 Copper and 3 Estate cards. +- Game ends when 3 supply piles or the Province pile is empty. Player with most victory points wins (fewest turns in case of a tie). +- Turn structure: Draw 5 cards → Action phase (play Action cards) → Buy phase (acquire new cards) → Clean-up phase (discard down to 7 cards). +- Players have 1 Action and 1 Buy per turn, but Action cards can modify these values. +- Deck is shuffled when empty. + +**Card Types:** +- Treasure: Copper, Silver, Gold +- Victory: Estate, Duchy, Province +- Curse +- Kingdom: Action, Action-Attack, Action-Reaction, Victory + +**Strategy:** +- Focus on acquiring powerful Action and Victory point cards. +- Manage Treasure effectively to outpace opponents. +- Efficiently use Action cards to draw more cards and perform additional actions. +- Plan ahead for Action card restrictions (e.g., cannot play Action cards for first two turns). +- Combine Treasure cards and Action-provided coins to buy multiple cards. +- Use Action cards to alter game rules (e.g., drawing more cards, playing more Action cards, buying additional cards). +- Understand specific card effects and timing (e.g., Throne Room, Village, Witch, Thief). +- Manage hand size and card discarding/trashing strategically. +- Recognize various strategic setups (e.g., "Big Money" with Market and Mine). + +**Notable Cards & Effects:** +- Throne Room: Play an Action card twice without using extra Actions. +- Village: Requires careful tracking of remaining Actions. +- Witch: Discards cards, manage hand size carefully. +- Thief: Reveal last card, discard one Treasure chosen by the attacker. +- Moat: Protects against Attack cards. +- Market, Laboratory: Allow additional card draws and plays. +- Mine: Enables Treasure upgrades. +- Gardens: Victory card with end-game scoring. +- Militia: Forces players to discard down to three cards. + +**Special Actions & Rules:** +- Trash Treasures to gain other Treasures or cheaper ones. +- Use Moat to avoid Attack effects. +- Specific conditions for Moneylender, Remodel, Smithy, Spy, and Thief. +- Resolve 'Chancellor' before other turn actions. +- Place gained cards directly into the Discard pile. \ No newline at end of file diff --git a/data/dominion/rulebook.pdf b/data/dominion/rulebook.pdf new file mode 100644 index 000000000..0d1e8e8b8 Binary files /dev/null and b/data/dominion/rulebook.pdf differ diff --git a/data/hearts/ruleSummary.txt b/data/hearts/ruleSummary.txt new file mode 100644 index 000000000..844b8a264 --- /dev/null +++ b/data/hearts/ruleSummary.txt @@ -0,0 +1,31 @@ +**Game Rules:** + +1. **Objective:** Avoid taking tricks containing hearts or the queen of spades. +2. **Deck:** Standard 52-card deck. +3. **Players:** 4. +4. **Scoring:** + - Hearts: 1 point each. + - Queen of Spades: 13 points. + - Other cards: 0 points. + - First player to reach 100 points loses. Lowest score wins. +5. **Passing:** + - Each player passes 3 cards to another player. + - Passing direction rotates clockwise each round. + +**Strategies:** + +1. **Queen of Spades:** + - Avoid leading with the queen of spades. + - Pass low spades to avoid being stuck with high spades alone. + +2. **Hearts:** + - Save high hearts (especially the queen, king, and ace of hearts) for late-game use to prevent opponents from shooting the moon. + - Try to avoid leading with hearts early in the game. + +3. **Trick Avoidance:** + - Avoid taking tricks with hearts or the queen of spades. + - If you must take a trick with points, try to take it from an opponent with a higher score. + +4. **Shooting the Moon:** + - Aim to take all hearts and the queen of spades in one hand to score 0 points and pass 26 points to each opponent. + - Be cautious, as failing to shoot the moon results in a high score. \ No newline at end of file diff --git a/data/hearts/rulebook.pdf b/data/hearts/rulebook.pdf new file mode 100644 index 000000000..2de5927f7 Binary files /dev/null and b/data/hearts/rulebook.pdf differ diff --git a/data/loveletter/ruleSummary.txt b/data/loveletter/ruleSummary.txt new file mode 100644 index 000000000..c9270a71c --- /dev/null +++ b/data/loveletter/ruleSummary.txt @@ -0,0 +1,31 @@ +**"Love Letter" Game Summary for Developer Implementation:** + +**Objective:** +- Be the last player standing or have the highest-value card when the deck runs out. + +**Player Count:** +- 2-6 players (Classic version: 2-4 players) + +**Gameplay:** +- Players draw and play one card per turn, each with unique effects. +- Effects can eliminate players, trade hands, peek at cards, or grant temporary immunity. +- The round ends when the deck is empty or one player remains. + +**Special Cards:** +- **Princess & Guard:** Immediately eliminate players. +- **Handmaid:** Provides temporary immunity. +- **Countess:** Must be played if King or Prince is in hand. +- **Spy:** Grants a favor token at round's end if played or discarded. + +**Strategy:** +- Pay attention to played/discarded cards and other players' hands. +- Manage your hand carefully, considering forced play conditions (Countess, Princess). +- Use cards like Chancellor to draw and optimize your hand. +- Protect yourself with Handmaid, eliminate low-card opponents with Baron, and bluff with Spy. + +**Winning:** +- Win a set number of rounds to claim victory. + +**Classic Version:** +- Excludes certain cards from the setup. +- Supports 2-4 players. \ No newline at end of file diff --git a/data/loveletter/rulebook.pdf b/data/loveletter/rulebook.pdf new file mode 100644 index 000000000..1746bb826 Binary files /dev/null and b/data/loveletter/rulebook.pdf differ diff --git a/data/poker/ruleSummary.txt b/data/poker/ruleSummary.txt new file mode 100644 index 000000000..17529a9be --- /dev/null +++ b/data/poker/ruleSummary.txt @@ -0,0 +1,29 @@ +**Texas Hold'em Game Rules and Strategy Summary:** + +**Objective:** Form the best five-card hand using any combination of two hole cards and five community cards. + +**Game Flow:** +1. **Button & Blinds:** + - The button (dealer) moves clockwise. + - Player left of the button posts the small blind; next player posts the big blind. + +2. **Hole Cards & Betting:** + - Each player receives two hole cards. + - Betting starts left of the big blind, with options to call, raise, or fold. + - Betting continues clockwise, with all remaining players matching the highest bet. + +3. **Community Cards & Betting Rounds:** + - **Flop:** Three community cards dealt, followed by a betting round. + - **Turn:** One more community card dealt, followed by a betting round. + - **River:** Final community card dealt, followed by the final betting round. + +4. **Hand Rankings (Best to Worst):** + - Royal Flush, Four of a Kind, Full House, Flush, Straight, Three of a Kind, Two Pair, Pair, High Card. + +**Winning the Pot:** +- The player with the highest-ranking hand wins the pot. +- In case of a tie, the pot is split. + +**Strategy:** +- Form a pair or a high card combination with more than one suit. +- Use any combination of hole cards and community cards to make the best five-card hand. \ No newline at end of file diff --git a/data/poker/rulebook.pdf b/data/poker/rulebook.pdf new file mode 100644 index 000000000..c1a40a155 Binary files /dev/null and b/data/poker/rulebook.pdf differ diff --git a/data/stratego/ruleSummary.txt b/data/stratego/ruleSummary.txt new file mode 100644 index 000000000..ca1c6055d --- /dev/null +++ b/data/stratego/ruleSummary.txt @@ -0,0 +1,34 @@ +**Stratego Game Rules and Strategy Summary** + +**Objective:** Capture the opponent's flag or render them unable to move. + +**Board:** 10x10 grid. + +**Pieces:** Each player has 40 pieces, each with a unique rank and movement ability. + +**Movement:** +- Most pieces move one space at a time. +- The Scout can move any distance. + +**Attacking:** +- Pieces reveal their rank when attacking. +- Higher ranked piece always captures the lower, except when equal ranks attack each other (both are removed). +- Special rules: + - Bomb: Captures adjacent pieces when attacked. + - Spy: Captures Marshal or Flag, but loses to any other piece or when attacked. + - Miner: Captures Bombs. + +**Endgame:** +- Game ends in draw if both flags are protected and only non-Miner pieces remain. + +**Strategy:** +- Concealment: Hide pieces' ranks to mislead the opponent. +- Bluffing: Make moves to deceive the opponent about your pieces' identities. +- Setup: Strategically place pieces at the start of the game for optimal defense and offense. + +**Pace:** Fast-paced, with no standard notation. Digital interfaces may record moves. + +**Unique Abilities:** +- Marshal: Most powerful. +- Spy: Weakest, but can capture Marshal or Flag. +- Other pieces have distinct movement abilities and ranks. \ No newline at end of file diff --git a/data/stratego/rulebook.pdf b/data/stratego/rulebook.pdf new file mode 100644 index 000000000..f1b84863d Binary files /dev/null and b/data/stratego/rulebook.pdf differ diff --git a/data/sushigo/ruleSummary.txt b/data/sushigo/ruleSummary.txt new file mode 100644 index 000000000..5c4dde745 --- /dev/null +++ b/data/sushigo/ruleSummary.txt @@ -0,0 +1,37 @@ +**Game Summary for Developer Implementation** + +**Game Rules:** +- Players draft cards simultaneously, keeping one and passing the rest to the left. This happens over three rounds. +- Special cards include: + - **Wasabi**: Boosts nigiri scores. + - **Chopsticks**: Allows taking two sushi cards on future turns. +- Pudding cards determine the game's end: + - Most pudding cards: +6 points. + - Least pudding cards: -6 points. + - Ties for least are broken by most pudding cards. +- Scoring is based on sets of: + - **Tempura**: 2 cards for 5 points. + - **Sashimi**: 3 cards for 10 points. + - **Dumplings**: More is better, up to 15 points for 5+ dumplings. + - **Nigiri**: 1-3 points, increased by wasabi. + - **Maki Rolls**: 6 points. + - **Pudding**: +6 for most, -6 for least. + - **Chopsticks**: No points. +- Two-player variant includes a third dummy player. + +**Strategy:** +- Focus on collecting high-value sets: + - **Sashimi**: Aim for sets of 3 for 10 points. + - **Tempura**: Aim for sets of 2 for 5 points. + - **Maki Rolls**: 6 points each. +- Use special cards strategically: + - **Wasabi**: Boost nigiri scores. + - **Chopsticks**: Maximize card collection on future turns. +- Manage pudding cards: + - Avoid having the least to prevent point deduction. + - Aim for having the most to gain points, but not at the expense of other high-scoring sets. +- In two-player games, consider the dummy player's impact on pudding card distribution. + +**End of Game:** +- The game ends after the final card is passed. +- The player with the highest score wins. In case of a tie, the player with the most pudding cards wins. \ No newline at end of file diff --git a/data/sushigo/rulebook.pdf b/data/sushigo/rulebook.pdf new file mode 100644 index 000000000..988bd97aa Binary files /dev/null and b/data/sushigo/rulebook.pdf differ diff --git a/data/tictactoe/ruleSummary.txt b/data/tictactoe/ruleSummary.txt new file mode 100644 index 000000000..4c7a875fc --- /dev/null +++ b/data/tictactoe/ruleSummary.txt @@ -0,0 +1,13 @@ +## Tic-Tac-Toe: Core Game Rules + +Tic-Tac-Toe is a simple yet strategic game played on a 3x3 grid. Two players, typically designated "X" and "O," take turns placing their mark on an empty square. The goal is to be the first to achieve a line of three of your own marks – either horizontally, vertically, or diagonally. + +**Here's a breakdown of the core gameplay:** + +* **The Grid:** The game is played on a 3x3 grid, often drawn on paper or a whiteboard. +* **Players:** Two players participate, one playing as "X" and the other as "O." +* **Turns:** Players alternate turns, placing their mark in an empty square on the grid. +* **Winning Condition:** The first player to achieve a line of three of their own marks (horizontally, vertically, or diagonally) wins the game. +* **Tie Game:** If all squares are filled without a player achieving three in a row, the game ends in a tie. + +**Simple and Engaging:** Tic-Tac-Toe is a quick and engaging game that can be enjoyed by people of all ages. It's a great way to introduce strategic thinking and develop problem-solving skills. diff --git a/data/tictactoe/rulebook.pdf b/data/tictactoe/rulebook.pdf new file mode 100644 index 000000000..ceb396fc0 Binary files /dev/null and b/data/tictactoe/rulebook.pdf differ diff --git a/data/virus/ruleSummary.txt b/data/virus/ruleSummary.txt new file mode 100644 index 000000000..c61581344 --- /dev/null +++ b/data/virus/ruleSummary.txt @@ -0,0 +1,27 @@ +**"Virus" is a strategic card game for 2-6 players, aiming to collect four different colored organ cards to form a healthy body. Here are the key rules and strategies:** + +**Game Rules:** +- Players draw three cards per turn. +- To win, a player must collect four unique organ cards. +- Card types: Healthy Organs, Viruses, Medicines, and Treatments/Actions. +- Viruses can destroy or infect organs; Medicines protect or immunize organs. +- Treatment/Action cards offer special effects, like Virus transfer ("Contagion") or organ swapping ("Organ Thief", "Organ Transplantation"). +- Multicolor cards can represent any color. +- The game ends when a player has four healthy organs. +- When the deck is empty, draw from the discard pile. + +**Strategy:** +- Protect your organs with Medicine cards. +- Use Virus cards strategically to target opponents' organs. +- Leverage Treatment/Action cards for disruption or advantage. +- Key strategies include: + - Spreading viruses to hinder opponents. + - Using "Medical Error" to swap bodies when an opponent is close to winning. + - Immunizing organs as soon as possible. + +**Implementation Notes:** +- Ensure each player has three cards in their hand at the start. +- Include virus, medicine, treatment/action, and multicolor cards in the deck. +- Implement special effects for Treatment/Action cards. +- Allow drawing from the discard pile when the deck is empty. +- End the game when a player collects four unique healthy organ cards. \ No newline at end of file diff --git a/data/virus/rulebook.pdf b/data/virus/rulebook.pdf new file mode 100644 index 000000000..043304300 Binary files /dev/null and b/data/virus/rulebook.pdf differ diff --git a/data/wonders7/ruleSummary.txt b/data/wonders7/ruleSummary.txt new file mode 100644 index 000000000..41bfd3ae6 --- /dev/null +++ b/data/wonders7/ruleSummary.txt @@ -0,0 +1,41 @@ +**Game Rules & Strategy Summary** + +**Game Rules:** +- Players lead cities through three ages, each with 6 turns. +- Players draft cards (structures, Wonders) and compare military strength with neighbors. +- Victory points (VP) are awarded at the end of each age (1, 3, 5 VP respectively). +- Cards have costs (coins, resources) and provide resources, points, coins, or military strength. +- Players buy resources from neighbors for 2 coins each. +- Players take turns playing cards, gaining coins, or advancing Wonders. +- Military conflicts resolve at the end of each age. +- Final score includes VP tokens, coins (3 coins = 1 VP), Wonder points, and structure points. +- Scientific structures earn points for sets of symbols. +- Two-player variant includes a shared "Free City." +- Each Wonder has unique abilities and VP values. + +**Strategy:** +- Focus on resource production, commerce, and military strength. +- Prioritize structures that provide resources, points, and free builds. +- Plan resource purchases and constructions carefully. +- Use commercial structures to reduce resource costs. +- Balance resource production, military strength, and coin management. +- Optimize Free City resource purchases and constructions in the two-player variant. +- Utilize each Wonder's unique abilities for resources, coins, or VP. +- Optimize resource use, timing of Wonder stages, and Guild selection. +- Build structures that provide market discounts or defensive bonuses. +- Balance immediate gains with long-term VP from card colors and Guilds. +- Build and manage your city to maximize VP and scientific symbols. + +**Key Points:** +- Efficient resource production and management are crucial. +- Balance immediate gains with long-term VP strategies. +- Utilize each Wonder's unique abilities effectively. +- Plan ahead for Free Constructions and coin income. +- Optimize resource purchases and mandated constructions in the Free City (two-player variant). + +**Development Notes:** +- Implement unique abilities for each Wonder. +- Ensure each card's cost, benefits, and effects are clearly defined. +- Incorporate military conflict resolution and VP awarding at the end of each age. +- Implement the two-player variant with the Free City mechanics. +- Ensure final scoring accurately reflects all VP sources (tokens, coins, Wonder points, structure points, scientific symbols). \ No newline at end of file diff --git a/data/wonders7/rulebook.pdf b/data/wonders7/rulebook.pdf new file mode 100644 index 000000000..58ab0713a Binary files /dev/null and b/data/wonders7/rulebook.pdf differ diff --git a/json/players/gameSpecific/ExplodingKittens.json b/json/players/gameSpecific/ExplodingKittens.json index 54859369c..a6b268981 100644 --- a/json/players/gameSpecific/ExplodingKittens.json +++ b/json/players/gameSpecific/ExplodingKittens.json @@ -1,7 +1,7 @@ { "budgetType": "BUDGET_TIME", "rolloutLength": 10, - "opponentTreePolicy": "SelfOnly", + "opponentTreePolicy": "OneTree", "MASTGamma": 0, "heuristic": { "class": "players.heuristics.OrdinalPosition" diff --git a/llm/__pycache__/config.cpython-39.pyc b/llm/__pycache__/config.cpython-39.pyc new file mode 100644 index 000000000..dda8367e5 Binary files /dev/null and b/llm/__pycache__/config.cpython-39.pyc differ diff --git a/pom.xml b/pom.xml index d39c529f2..90966389b 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,12 @@ com.google.code.gson gson - 2.9.0 + 2.10 + + + com.google.guava + guava + 33.2.1-jre com.github.davidmoten @@ -69,17 +74,27 @@ org.apache.spark spark-core_2.13 - 3.3.1 + 3.5.1 org.apache.spark spark-sql_2.13 - 3.3.1 + 3.5.1 + + + com.formdev + flatlaf + 3.4.1 + + + com.github.javaparser + javaparser-core + 3.26.1 org.apache.spark spark-mllib_2.13 - 3.3.1 + 3.5.1 commons-io @@ -122,6 +137,36 @@ RELEASE compile + + dev.langchain4j + langchain4j + 0.35.0 + + + dev.langchain4j + langchain4j-vertex-ai-gemini + 0.35.0 + + + dev.langchain4j + langchain4j-vertex-ai + 0.35.0 + + + dev.langchain4j + langchain4j-mistral-ai + 0.35.0 + + + dev.langchain4j + langchain4j-open-ai + 0.35.0 + + + dev.langchain4j + langchain4j-anthropic + 0.35.0 + @@ -250,7 +295,13 @@ - + + + maven_central + Maven Central + https://repo.maven.apache.org/maven2/ + + diff --git a/src/main/java/core/AbstractForwardModel.java b/src/main/java/core/AbstractForwardModel.java index dfc42b888..989d5fa66 100644 --- a/src/main/java/core/AbstractForwardModel.java +++ b/src/main/java/core/AbstractForwardModel.java @@ -15,6 +15,7 @@ public abstract class AbstractForwardModel { public ActionTreeNode root; + public List leaves; // Decorator modify (restrict) the actions available to the player. // This enables the Forward Model to be passed to the decision algorithm (e.g. MCTS), and ensure that any diff --git a/src/main/java/core/AbstractGameState.java b/src/main/java/core/AbstractGameState.java index d4f9c5c77..42c93eea4 100644 --- a/src/main/java/core/AbstractGameState.java +++ b/src/main/java/core/AbstractGameState.java @@ -20,7 +20,7 @@ import java.util.function.Function; import java.util.function.Supplier; -import static core.CoreConstants.GameResult.GAME_ONGOING; +import static core.CoreConstants.GameResult.*; /** * Contains all game state information. @@ -647,4 +647,20 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(playerResults); return result; } + + public boolean isGameOver() + { + return gameStatus.equals(GAME_END); + } + + public int getWinner() + { + if(gameStatus.equals(GAME_END)){ + for(int playerId = 0; playerId < nPlayers; playerId++) + if(playerResults[playerId] == WIN_GAME) + return playerId; + } + return -1; + } + } diff --git a/src/main/java/core/AbstractParameters.java b/src/main/java/core/AbstractParameters.java index 2dfd4a8be..197ef72ee 100644 --- a/src/main/java/core/AbstractParameters.java +++ b/src/main/java/core/AbstractParameters.java @@ -1,8 +1,10 @@ package core; +import core.interfaces.IStateHeuristic; import core.interfaces.ITunableParameters; import evaluation.optimisation.TunableParameters; import games.GameType; +import players.heuristics.NullHeuristic; import java.util.*; @@ -209,4 +211,6 @@ static public AbstractParameters createFromFile(GameType game, String fileName) throw new AssertionError("JSON parameter initialisation not supported for " + game); } } + + public IStateHeuristic getStateHeuristic() { return new NullHeuristic(); } } diff --git a/src/main/java/core/AbstractPlayer.java b/src/main/java/core/AbstractPlayer.java index 3a1739d1d..308839bf2 100644 --- a/src/main/java/core/AbstractPlayer.java +++ b/src/main/java/core/AbstractPlayer.java @@ -91,7 +91,6 @@ public void setForwardModel(AbstractForwardModel model) { model.addPlayerDecorator(decorator); } } - /** * Retrieves the forward model for current game being played. * diff --git a/src/main/java/core/DecoratedForwardModel.java b/src/main/java/core/DecoratedForwardModel.java new file mode 100644 index 000000000..b407ed676 --- /dev/null +++ b/src/main/java/core/DecoratedForwardModel.java @@ -0,0 +1,61 @@ +package core; + +import core.actions.AbstractAction; +import core.interfaces.IPlayerDecorator; + +import java.util.ArrayList; +import java.util.List; + +public class DecoratedForwardModel extends AbstractForwardModel { + + + // This wraps a Forward Model in one or more Decorators that modify (restrict) the actions available to the player. + // This enables the Forward Model to be passed to the decision algorithm (e.g. MCTS), and ensure that any + // restrictions are applied to the actions available to the player during search, and not just + // in the main game loop. + + // most function calls are forwarded to the wrapped forward model, except for + // _computeAvailableActions, to which we first apply the decorators + + final List decorators; + final AbstractForwardModel wrappedFM; + final int decisionPlayerID; + + public DecoratedForwardModel(AbstractForwardModel forwardModel, List decorators, int playerID) { + this.wrappedFM = forwardModel; + this.decorators = new ArrayList<>(decorators); + this.decisionPlayerID = playerID; + } + + @Override + protected void _setup(AbstractGameState firstState) { + wrappedFM._setup(firstState); + } + + @Override + protected void _next(AbstractGameState currentState, AbstractAction action) { + wrappedFM._next(currentState, action); + } + + @Override + protected List _computeAvailableActions(AbstractGameState gameState) { + List actions = wrappedFM.computeAvailableActions(gameState); + for (IPlayerDecorator decorator : decorators) { + if (decorator.decisionPlayerOnly() && gameState.getCurrentPlayer() != decisionPlayerID) + continue; + actions = decorator.actionFilter(gameState, actions); + } + return actions; + } + + @Override + protected AbstractForwardModel _copy() { + return wrappedFM._copy(); + } + + @Override + protected void endPlayerTurn(AbstractGameState state) { + wrappedFM.endPlayerTurn(state); + } + +} diff --git a/src/main/java/core/LLMTest.java b/src/main/java/core/LLMTest.java new file mode 100644 index 000000000..f1f420d9c --- /dev/null +++ b/src/main/java/core/LLMTest.java @@ -0,0 +1,94 @@ +package core; + +import games.GameType; +import players.basicMCTS.BasicMCTSParams; +import players.basicMCTS.BasicMCTSPlayer; +import players.heuristics.StringHeuristic; +import players.human.ActionController; +import players.simple.OSLAPlayer; +import utilities.Utils; + +import java.util.ArrayList; + + +public class LLMTest { + + public enum Agent + { + OSLA, + MCTS; + + Agent(){} + + public AbstractPlayer createPlayer(String evaluatorFileName, String className) { + if (this == OSLA){ + return new OSLAPlayer(new StringHeuristic(evaluatorFileName, className)); + } + else if (this == MCTS){ + BasicMCTSParams params = new BasicMCTSParams(); + params.heuristic = new StringHeuristic(evaluatorFileName, className); + return new BasicMCTSPlayer(params); + } + return null; + } + } + + public static void main(String[] args) { +// evaluate(args, "LoveLetter", "llm/LoveLetterEvaluator.java", "LoveLetterEvaluator", Agent.OSLA); + evaluate(args, "TicTacToe", "llm/TicTacToeEvaluator.java", "TicTacToeEvaluator", Agent.OSLA); + } + + + /** + * The recommended way to run a game is via evaluations.Frontend, however that may not work on + * some games for some screen sizes due to the vagaries of Java Swing... + *

+ * Test class used to run a specific game. The user must specify: + * 1. Action controller for GUI interactions / null for no visuals + * 2. Random seed for the game + * 3. Players for the game + * 4. Game parameter configuration + * 5. Mode of running + * and then run this class. + */ + private static void evaluate (String args[], String gameStr, String evaluatorFileName, String className, Agent agent) + { + GameType gameType = GameType.valueOf(Utils.getArg(args, "game", gameStr)); + boolean useGUI = Utils.getArg(args, "gui", true); + int turnPause = Utils.getArg(args, "turnPause", 0); + long seed = Utils.getArg(args, "seed", System.currentTimeMillis()); + ActionController ac = new ActionController(); + + /* Set up players for the game */ + ArrayList players = new ArrayList<>(); +// players.add(new MCTSPlayer()); + players.add(new OSLAPlayer()); + players.add(agent.createPlayer(evaluatorFileName, className)); + +// MCTSParams params = new MCTSParams(); +// params.heuristic = new StringHeuristic(); +// players.add(new MCTSPlayer(params)); + + int nGames = 20; + int wins = 0; + int ties = 0; + int playerToMonitor = 1; + for (int i = 0; i < nGames; i++) { + Game game = gameType.createGameInstance(players.size()); + game.reset(players); + game.run(); + CoreConstants.GameResult[] results = game.getGameState().getPlayerResults(); + if (results[playerToMonitor] == CoreConstants.GameResult.WIN_GAME) { + wins++; + } else if (results[playerToMonitor] == CoreConstants.GameResult.DRAW_GAME) { + ties++; + } + } + System.out.println(wins*1.0/nGames + "," + ties*1.0/nGames); + + // TODO human in the loop with GUI + } + + + +} diff --git a/src/main/java/core/PyTAG.java b/src/main/java/core/PyTAG.java index 2148140f9..7d28ebea0 100644 --- a/src/main/java/core/PyTAG.java +++ b/src/main/java/core/PyTAG.java @@ -18,7 +18,7 @@ import players.simple.RandomPlayer; import utilities.ActionTreeNode; -import games.explodingkittens.*; +import games.explodingkittensOLD.*; import java.util.ArrayList; diff --git a/src/main/java/core/actions/AbstractAction.java b/src/main/java/core/actions/AbstractAction.java index fce3b9164..11cad70f4 100644 --- a/src/main/java/core/actions/AbstractAction.java +++ b/src/main/java/core/actions/AbstractAction.java @@ -51,7 +51,7 @@ public String getString(AbstractGameState gs, int perspectivePlayer) { * The GUI formally supports multiple players viewing a game. This in practice is only going to be used * for games with (near) perfect information. For games that actually implement hidden information in * a move (Resistance, Hanabi, Sushi Go, etc), we will only need the game actions to implement - * getString(AbstractGameState, int). This is a helper method to make this downstream imnplementation + * getString(AbstractGameState, int). This is a helper method to make this downstream implementation * easier without trying to puzzle out what it means to have multiple players viewing a game with hidden information. * * @param gs diff --git a/src/main/java/core/components/Deck.java b/src/main/java/core/components/Deck.java index 51572beeb..9763df3b3 100644 --- a/src/main/java/core/components/Deck.java +++ b/src/main/java/core/components/Deck.java @@ -19,8 +19,8 @@ * A deck is defined as a "group of components". Examples of decks are: * * A hand of components * * A deck to draw from - * * components played on the player's area - * * Discomponent pile + * * Components played on the player's area + * * Discard pile */ public class Deck extends Component implements IComponentContainer, Iterable { diff --git a/src/main/java/core/components/GridBoard.java b/src/main/java/core/components/GridBoard.java index 171588fcd..c7ed74804 100644 --- a/src/main/java/core/components/GridBoard.java +++ b/src/main/java/core/components/GridBoard.java @@ -19,6 +19,11 @@ import static core.CoreConstants.imgHash; import static utilities.Utils.getNeighbourhood; +/** + * GridBoard is a 2D grid of Components. It can be used to represent a board in a game, a map, or any other 2D grid. + * Each cell on the grid can contain a Component of any type. + * @param + */ public class GridBoard extends Component implements IComponentContainer { private int width; // Width of the board diff --git a/src/main/java/evaluation/RunArg.java b/src/main/java/evaluation/RunArg.java index f89a33c2b..e52244bce 100644 --- a/src/main/java/evaluation/RunArg.java +++ b/src/main/java/evaluation/RunArg.java @@ -2,8 +2,13 @@ import org.apache.hadoop.yarn.webapp.hamlet.HamletSpec; import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; import java.util.*; import static java.util.stream.Collectors.toList; @@ -232,9 +237,35 @@ public static Map parseConfig(String[] args, List usages) public static Map parseConfig(String[] args, List usages, boolean checkUnknownArgs) { if (checkUnknownArgs) checkUnknownArgs(args, usages); - return Arrays.stream(RunArg.values()) + Map commandLineArgs = Arrays.stream(RunArg.values()) .filter(arg -> usages.stream().anyMatch(arg::isUsedIn)) .collect(toMap(arg -> arg, arg -> arg.parse(args))); + // then as a second stage we unpack any arguments specified in the config file + String setupFile = commandLineArgs.getOrDefault(RunArg.config, "").toString(); + if (!setupFile.isEmpty()) { + // Read from file in addition (any values on the command line override the file contents) + try { + FileReader reader = new FileReader(setupFile); + JSONParser parser = new JSONParser(); + JSONObject json = (JSONObject) parser.parse(reader); + Map configFileArgs = parseConfig(json, usages); + for (RunArg key : configFileArgs.keySet()) { + // we check if the key is already in the command line arguments (we check args directly as + // parseConfig populates all the Map with the default value if not present in the array) + if (Arrays.stream(args).noneMatch(s -> s.startsWith(key.name()))) { + commandLineArgs.put(key, configFileArgs.get(key)); + } else { + System.out.println("Using command line value for " + key + " of " + commandLineArgs.get(key)); + } + } + } catch (FileNotFoundException ignored) { + throw new AssertionError("Config file not found : " + setupFile); + // parseConfig(runGames, args); + } catch (IOException | ParseException e) { + throw new RuntimeException(e); + } + } + return commandLineArgs; } public static void checkUnknownArgs(String[] args, List usages) { diff --git a/src/main/java/evaluation/RunGames.java b/src/main/java/evaluation/RunGames.java index 6c39bd464..5bb1ce524 100644 --- a/src/main/java/evaluation/RunGames.java +++ b/src/main/java/evaluation/RunGames.java @@ -57,27 +57,11 @@ public static void main(String[] args) { RunGames runGames = new RunGames(); runGames.config = parseConfig(args, Collections.singletonList(Usage.RunGames)); - String setupFile = runGames.config.getOrDefault(RunArg.config, "").toString(); - if (!setupFile.isEmpty()) { - // Read from file instead - try { - FileReader reader = new FileReader(setupFile); - JSONParser parser = new JSONParser(); - JSONObject json = (JSONObject) parser.parse(reader); - runGames.config = parseConfig(json, Usage.RunGames); - } catch (FileNotFoundException ignored) { - throw new AssertionError("Config file not found : " + setupFile); - // parseConfig(runGames, args); - } catch (IOException | ParseException e) { - throw new RuntimeException(e); - } - } runGames.initialiseGamesAndPlayerCount(); if (!runGames.config.get(RunArg.gameParams).equals("") && runGames.gamesAndPlayerCounts.keySet().size() > 1) throw new IllegalArgumentException("Cannot yet provide a gameParams argument if running multiple games"); // 2. Setup - LinkedList agents = new LinkedList<>(); if (!runGames.config.get(playerDirectory).equals("")) { agents.addAll(PlayerFactory.createPlayers((String) runGames.config.get(playerDirectory))); diff --git a/src/main/java/evaluation/tournaments/RoundRobinTournament.java b/src/main/java/evaluation/tournaments/RoundRobinTournament.java index 856c11c68..6aacb571c 100644 --- a/src/main/java/evaluation/tournaments/RoundRobinTournament.java +++ b/src/main/java/evaluation/tournaments/RoundRobinTournament.java @@ -559,6 +559,11 @@ protected void reportResults() { // To file if (toFile) { try { + File resultsFile = new File(this.resultsFile); + if (!resultsFile.exists()) { + File dir = resultsFile.getParentFile(); + dir.mkdirs(); + } FileWriter writer = new FileWriter(resultsFile, true); for (String line : dataDump) writer.write(line); @@ -737,11 +742,11 @@ protected double[] reportAlphaRank(List dataDump, int[][] values) { } public double getWinRate(int agentID) { - return finalWinRanking.get(agentID).a; + return finalWinRanking.get(agentID) == null ? 0.0 : finalWinRanking.get(agentID).a; } public double getWinStdErr(int agentID) { - return finalWinRanking.get(agentID).b; + return finalWinRanking.get(agentID) == null ? 0.0 : finalWinRanking.get(agentID).b; } public double getOrdinalRank(int agentID) { diff --git a/src/main/java/games/GameType.java b/src/main/java/games/GameType.java index c62fab68e..a5e2bb786 100644 --- a/src/main/java/games/GameType.java +++ b/src/main/java/games/GameType.java @@ -70,10 +70,12 @@ import gametemplate.GTGameState; import gametemplate.GTParameters; import gui.*; +import llm.DocumentSummariser; import org.apache.commons.lang3.reflect.ConstructorUtils; import players.human.ActionController; import players.human.HumanGUIPlayer; +import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.*; @@ -254,6 +256,40 @@ public enum GameType { this(minPlayers, maxPlayers, categories, mechanics, gameStateClass, forwardModelClass, parameterClass, guiManagerClass, null); } + public String loadRulebook() { + String pdfFilePath = "data/" + this.name().toLowerCase() + "/rulebook.pdf"; + String ruleSummaryPath = "data/" + this.name().toLowerCase() + "/ruleSummary.txt"; + // The first time we process the rulebook we create rule and strategy summaries for use + // with LLM-created heuristics (etc.) + + File ruleSummaryFile = new File(ruleSummaryPath); + if (ruleSummaryFile.exists()) { + try { + Scanner scanner = new Scanner(ruleSummaryFile); + StringBuilder sb = new StringBuilder(); + while (scanner.hasNextLine()) { + sb.append(scanner.nextLine()).append("\n"); + } + return sb.toString(); + } catch (FileNotFoundException e) { + throw new AssertionError("File exists but could not be read: " + ruleSummaryPath); + } + } + + DocumentSummariser summariser = new DocumentSummariser(pdfFilePath); + String rulesText = summariser.processText("game rules and strategy", 500); + // Then write this to file + try { + BufferedWriter writer = new BufferedWriter(new FileWriter(ruleSummaryPath)); + writer.write(rulesText); + writer.close(); + } catch (IOException e) { + throw new AssertionError("Error writing rule summary file: " + ruleSummaryPath); + } + + return rulesText; + } + // Getters public int getMinPlayers() { return minPlayers; @@ -275,6 +311,22 @@ public String getDataPath() { return dataPath; } + public Class getGameStateClass() { + return gameStateClass; + } + + public Class getForwardModelClass() { + return forwardModelClass; + } + + public Class getGuiManagerClass() { + return guiManagerClass; + } + + public Class getParameterClass() { + return parameterClass; + } + public AbstractGameState createGameState(AbstractParameters params, int nPlayers) { if (gameStateClass == null) throw new AssertionError("No game state class declared for the game: " + this); try { diff --git a/src/main/java/games/cantstop/CantStopGameState.java b/src/main/java/games/cantstop/CantStopGameState.java index e02d3d3aa..91d4ff4a9 100644 --- a/src/main/java/games/cantstop/CantStopGameState.java +++ b/src/main/java/games/cantstop/CantStopGameState.java @@ -48,17 +48,23 @@ public void rollDice() { dice.forEach(d -> d.roll(rnd)); } - public boolean trackComplete(int n) { + /** + * True if the specified number track is complete and has been scored + **/ + public boolean isTrackComplete(int n) { return completedColumns[n]; } public void moveMarker(int n) { - if (!trackComplete(n)) { + if (!isTrackComplete(n)) { int currentPosition = temporaryMarkerPositions.getOrDefault(n, playerMarkerPositions[getCurrentPlayer()][n]); temporaryMarkerPositions.put(n, currentPosition + 1); } } + /** + * Returns the current dice roll as an int[2] + **/ public int[] getDice() { return dice.stream().mapToInt(Dice::getValue).toArray(); } @@ -71,14 +77,23 @@ public void setDice(int[] numbers) { } } + /** + * The current marker position for the specified player and number track + **/ public int getMarkerPosition(int number, int player) { return playerMarkerPositions[player][number]; } + /** + * The temporary marker position for the current player on the specified number track + **/ public int getTemporaryMarkerPosition(int number) { return temporaryMarkerPositions.getOrDefault(number, 0); } + /** + * The numbers of the tracks that have had markers moved in the current turn + **/ public List getMarkersMoved() { return new ArrayList<>(temporaryMarkerPositions.keySet()); } @@ -184,7 +199,7 @@ public void printToConsole() { sb.append("Max\n"); for (int n = 2; n <= 12; n++) { sb.append(String.format("%2d :\t", n)); - if (trackComplete(n)) + if (isTrackComplete(n)) sb.append(" COMPLETED"); else { for (int p = 0; p < getNPlayers(); p++) { diff --git a/src/main/java/games/cantstop/actions/AllocateDice.java b/src/main/java/games/cantstop/actions/AllocateDice.java index 05e3ebfd3..e766bb849 100644 --- a/src/main/java/games/cantstop/actions/AllocateDice.java +++ b/src/main/java/games/cantstop/actions/AllocateDice.java @@ -27,7 +27,7 @@ public boolean isLegal(CantStopGameState state) { Map numberCounts = Arrays.stream(numberSplit).boxed().collect(groupingBy(n -> n, counting())); for (int n : numberSplit) { int markerPosition = Math.max(state.getTemporaryMarkerPosition(n), state.getMarkerPosition(n, state.getCurrentPlayer())); - boolean canMoveOnTrack = !state.trackComplete(n) && (markerPosition + numberCounts.get(n) - 1) < params.maxValue(n); + boolean canMoveOnTrack = !state.isTrackComplete(n) && (markerPosition + numberCounts.get(n) - 1) < params.maxValue(n); retValue = canMoveOnTrack && retValue; } // then each number must either have a marker already, or a spare marker is available diff --git a/src/main/java/games/catan/CatanGameState.java b/src/main/java/games/catan/CatanGameState.java index 1566c7661..097c10020 100644 --- a/src/main/java/games/catan/CatanGameState.java +++ b/src/main/java/games/catan/CatanGameState.java @@ -41,6 +41,7 @@ public class CatanGameState extends AbstractGameState { public int negotiationStepsCount; public int nTradesThisTurn; + // Details of the current trade offer (if any, may be null) public AbstractAction getTradeOffer() { return tradeOffer; } @@ -142,6 +143,7 @@ public void setRollValue(int rollValue) { this.rollValue = rollValue; } + // value of the currently rolled dice public int getRollValue() { return rollValue; } @@ -179,6 +181,7 @@ public void addKnight(int playerID) { } } + // The number of knights that each player has public int[] getKnights() { return Arrays.copyOf(knights, knights.length); } @@ -237,6 +240,7 @@ public Map getExchangeRates(int playerID) { return exchangeRates.get(playerID); } + // The road distance between two adjacent settlements, one at x, y, the other along the specified edgeIdx public int getRoadDistance(int x, int y, int edgeIdx) { // As the settlements are the nodes, we expand them to find roads // calculates the distance length of the road @@ -305,6 +309,7 @@ private Set expandRoad(Set roadSet, List unexpanded, List< return expandRoad(roadSet, unexpanded, expanded); } + // The number of resource cards in a player's hand public int getNResourcesInHand(int player) { int deckSize = 0; for (Map.Entry e: playerResources.get(player).entrySet()) { diff --git a/src/main/java/games/coltexpress/ColtExpressForwardModel.java b/src/main/java/games/coltexpress/ColtExpressForwardModel.java index 24b3dbada..b1cab898d 100644 --- a/src/main/java/games/coltexpress/ColtExpressForwardModel.java +++ b/src/main/java/games/coltexpress/ColtExpressForwardModel.java @@ -112,7 +112,7 @@ private void setupRounds(ColtExpressGameState cegs, ColtExpressParameters cep) { } cegs.rounds.shuffle(rndForRoundCards); - RoundCard endCard = cegs.getRandomEndRoundCard(cep, rndForRoundCards); + RoundCard endCard = cegs.randomEndRoundCard(cep, rndForRoundCards); cegs.rounds.addToBottom(endCard); } diff --git a/src/main/java/games/coltexpress/ColtExpressGameState.java b/src/main/java/games/coltexpress/ColtExpressGameState.java index b23de27ec..2cd5febc5 100644 --- a/src/main/java/games/coltexpress/ColtExpressGameState.java +++ b/src/main/java/games/coltexpress/ColtExpressGameState.java @@ -208,7 +208,7 @@ protected AbstractGameStateWithTurnOrder __copy(int playerId) { for (int i = 0; i < rounds.getSize(); i++) { if (!rounds.isComponentVisible(i, playerId)) { if (i == rounds.getSize() - 1) { // last card, so use an End Round Card - copy.rounds.setComponent(i, getRandomEndRoundCard((ColtExpressParameters) getGameParameters(), null)); + copy.rounds.setComponent(i, randomEndRoundCard((ColtExpressParameters) getGameParameters(), null)); } else { copy.rounds.setComponent(i, getRandomRoundCard((ColtExpressParameters) getGameParameters(), i, exclusionList)); exclusionList.add(copy.rounds.get(i)); @@ -225,6 +225,10 @@ protected double _getHeuristicScore(int playerId) { return new ColtExpressHeuristic().evaluateState(this, playerId); } + /** + * The score that playerID would have if the game ended immediately. + * This includes the value of all loot, and the possible bonus shooter reward. + */ @Override public double getGameScore(int playerId) { double retValue = getLoot(playerId).sumInt(Loot::getValue); @@ -357,7 +361,6 @@ void distributeCards(){ } } - // Getters, setters public LinkedList getTrainCompartments() { return trainCompartments; } @@ -366,10 +369,17 @@ public Deck getLoot(int playerID) { return playerLoot.get(playerID); } + /** + * The Deck of played cards that will be executed in order in the execution phase + * Only some of these are visible. + */ public PartialObservableDeck getPlannedActions() { return plannedActions; } + /** + * The draw decks of each player (a List in playerID order) + */ public List> getPlayerDecks() { return playerDecks; } @@ -378,14 +388,24 @@ public PartialObservableDeck getRounds() { return rounds; } + /** + * A mapping of playerID to the character they are playing + */ public HashMap getPlayerCharacters() { return playerCharacters; } + /** + * The hands of each player (a List in playerID order) + * Only a player's own hand is visible + */ public List> getPlayerHandCards() { return playerHandCards; } + /** + * An array of the number of bullets each player has remaining + */ public int[] getBulletsLeft() { return bulletsLeft; } @@ -451,13 +471,13 @@ public String printTrain() { * Helper getter methods for round card composition. */ - RoundCard getRandomEndRoundCard(ColtExpressParameters cep, Random overrideRnd) { + RoundCard randomEndRoundCard(ColtExpressParameters cep, Random overrideRnd) { int nEndCards = cep.endRoundCards.length; int choice = overrideRnd == null ? rnd.nextInt(nEndCards) : overrideRnd.nextInt(nEndCards); return getEndRoundCard(cep, choice); } - RoundCard getEndRoundCard(ColtExpressParameters cep, int idx) { + private RoundCard getEndRoundCard(ColtExpressParameters cep, int idx) { if (idx >= 0 && idx < cep.endRoundCards.length) { RoundCard.TurnType[] turnTypes = cep.endRoundCards[idx].getTurnTypeSequence(); RoundEvent event = cep.endRoundCards[idx].getEndCardEvent(); @@ -466,7 +486,7 @@ RoundCard getEndRoundCard(ColtExpressParameters cep, int idx) { return null; } - RoundCard getRandomRoundCard(ColtExpressParameters cep, int i, List exclusionList) { + private RoundCard getRandomRoundCard(ColtExpressParameters cep, int i, List exclusionList) { List namesToExclude = exclusionList.stream().map(RoundCard::getComponentName).collect(toList()); List availableTypes = Arrays.stream(cep.roundCards) .filter(rc -> !namesToExclude.contains(rc.name())).collect(toList()); diff --git a/src/main/java/games/coltexpress/components/Loot.java b/src/main/java/games/coltexpress/components/Loot.java index d8df4c292..4639bf491 100644 --- a/src/main/java/games/coltexpress/components/Loot.java +++ b/src/main/java/games/coltexpress/components/Loot.java @@ -29,6 +29,7 @@ public static Loot createStrongbox(){ return new Loot(ColtExpressTypes.LootType.Strongbox, 1000); } + // point value of the loot public int getValue(){ return value; } diff --git a/src/main/java/games/coltexpress/gui/ColtExpressGUIManager.java b/src/main/java/games/coltexpress/gui/ColtExpressGUIManager.java index d5036c7ee..3032191aa 100644 --- a/src/main/java/games/coltexpress/gui/ColtExpressGUIManager.java +++ b/src/main/java/games/coltexpress/gui/ColtExpressGUIManager.java @@ -237,7 +237,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { // Update decks and visibility ColtExpressGameState cegs = (ColtExpressGameState)gameState; for (int i = 0; i < gameState.getNPlayers(); i++) { - playerHands[i].update((ColtExpressGameState) gameState, humanPlayerId); + playerHands[i].update((ColtExpressGameState) gameState, humanPlayerIds); // Highlight active player if (i == gameState.getCurrentPlayer()) { @@ -251,7 +251,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { plannedActions.updateComponent(cegs.getPlannedActions()); int activePlayer = player != null? (gameState.getCoreGameParameters().alwaysDisplayCurrentPlayer || gameState.getCoreGameParameters().alwaysDisplayFullObservable? player.getPlayerID(): - humanPlayerId.contains(player.getPlayerID())? player.getPlayerID():-1) : -1; + humanPlayerIds.contains(player.getPlayerID())? player.getPlayerID():-1) : -1; plannedActions.informActivePlayer(activePlayer); // Show planned actions from the first played diff --git a/src/main/java/games/connect4/Connect4GameState.java b/src/main/java/games/connect4/Connect4GameState.java index 6ad289a8d..43f0b36d6 100644 --- a/src/main/java/games/connect4/Connect4GameState.java +++ b/src/main/java/games/connect4/Connect4GameState.java @@ -29,6 +29,14 @@ public Connect4GameState(AbstractParameters gameParameters, int nPlayers) { gridBoard = null; } + /** + * This returns the player id of the token at the given position. Or -1 if this is empty. + */ + public int getPlayerAt(int x, int y) { + Token token = gridBoard.getElement(x, y); + return token == null ? -1 : token.getOwnerId(); + } + @Override protected GameType _getGameType() { return GameType.Connect4; @@ -59,12 +67,7 @@ protected double _getHeuristicScore(int playerId) { } /** - * This provides the current score in game turns. This will only be relevant for games that have the concept - * of victory points, etc. - * If a game does not support this directly, then just return 0.0 - * - * @param playerId - * @return - double, score of current state + * Score is not relevant for Connect4. This will be 0.0 if a game is nto finished. */ @Override public double getGameScore(int playerId) { diff --git a/src/main/java/games/diamant/DiamantGameState.java b/src/main/java/games/diamant/DiamantGameState.java index 5e5d6d9a3..e8947efde 100644 --- a/src/main/java/games/diamant/DiamantGameState.java +++ b/src/main/java/games/diamant/DiamantGameState.java @@ -148,12 +148,7 @@ protected double _getHeuristicScore(int playerId) return new DiamantHeuristic().evaluateState(this, playerId); } /** - * This provides the current score in game turns. This will only be relevant for games that have the concept - * of victory points, etc. - * If a game does not support this directly, then just return 0.0 - * - * @param playerId - * @return - double, score of current state + * Current score */ @Override public double getGameScore(int playerId) { @@ -216,7 +211,6 @@ protected boolean _equals(Object o) /** * Returns the number of player already in the cave */ - public int getNPlayersInCave() { int n = 0; @@ -264,7 +258,7 @@ public void printToConsole() { } } - private int getNHazardCardsInMainDeck(DiamantCard.HazardType ht) + public int getNHazardCardsInMainDeck(DiamantCard.HazardType ht) { int n = 0; for (int i=0; i _computeAvailableActions(AbstractGameState gameSt switch (state.getGamePhase().toString()) { case "Play": - if (state.actionsLeft() > 0) { + if (state.getActionsLeft() > 0) { List actionCards = state.getDeck(DeckType.HAND, playerID).stream() .filter(DominionCard::isActionCard).collect(toList()); List availableActions = actionCards.stream() @@ -166,8 +166,8 @@ protected List _computeAvailableActions(AbstractGameState gameSt return Collections.singletonList(new EndPhase(DominionGameState.DominionGamePhase.Play)); case "Buy": // we return every available card for purchase within our price range - int budget = state.availableSpend(playerID); - List options = state.cardsToBuy().stream() + int budget = state.getAvailableSpend(playerID); + List options = state.getCardsToBuy().stream() .filter(ct -> ct.cost <= budget) .sorted(Comparator.comparingInt(c -> -c.cost)) .map(ct -> new BuyCard(ct, playerID)) diff --git a/src/main/java/games/dominion/DominionGameState.java b/src/main/java/games/dominion/DominionGameState.java index 338fa76d3..483136db4 100644 --- a/src/main/java/games/dominion/DominionGameState.java +++ b/src/main/java/games/dominion/DominionGameState.java @@ -122,7 +122,10 @@ public boolean moveCard(DominionCard cardToMove, int fromPlayer, DeckType fromDe return false; } - public int actionsLeft() { + /** + * The number of actions the current player has left + */ + public int getActionsLeft() { return actionsLeftForCurrentPlayer; } @@ -130,7 +133,10 @@ public void changeActions(int delta) { actionsLeftForCurrentPlayer += delta; } - public int buysLeft() { + /** + * The number of buys the current player has left + */ + public int getBuysLeft() { return buysLeftForCurrentPlayer; } @@ -146,7 +152,10 @@ public void changeAdditionalSpend(int delta) { additionalSpendAvailable += delta; } - public int availableSpend(int playerID) { + /** + * How much money does the player have available to spend? + */ + public int getAvailableSpend(int playerID) { if (playerID != turnOwner) { return 0; } @@ -175,6 +184,10 @@ protected List _getAllComponents() { return components; } + /** + * Get the Deck of the specified type for the specified player. + * DeckType.ALL is not valid + */ public Deck getDeck(DeckType deck, int playerId) { switch (deck) { case HAND: @@ -191,6 +204,10 @@ public Deck getDeck(DeckType deck, int playerId) { throw new AssertionError("Unknown deck type " + deck); } + /** + * How many cards of the specified type are in the player's deck? + * Use DeckType.ALL to count all cards in all decks. + */ public int cardsOfType(CardType type, int playerId, DeckType deck) { Deck allCards; switch (deck) { @@ -217,7 +234,7 @@ public int cardsOfType(CardType type, int playerId, DeckType deck) { return (int) allCards.stream().filter(c -> c.cardType() == type).count(); } - public List cardsToBuy() { + public List getCardsToBuy() { return cardsIncludedInGame.keySet().stream() .filter(c -> cardsIncludedInGame.get(c) > 0) .sorted(comparingInt(c -> -c.cost)) @@ -325,22 +342,23 @@ protected double _getHeuristicScore(int playerId) { } /** - * This provides the current score in game turns. This will only be relevant for games that have the concept - * of victory points, etc. - * If a game does not support this directly, then just return 0.0 - * - * @param playerId Player number - * @return - double, score of current state + * This provides the current score for the specified player. */ @Override public double getGameScore(int playerId) { return getTotal(playerId, c -> c.victoryPoints(playerId, this)); } + /** + * This provides the total value of the specified deck for the player using the provided cardValuer function + */ public int getTotal(int playerId, DeckType deck, Function cardValuer) { return getDeck(deck, playerId).sumInt(cardValuer); } + /** + * This provides the total value across all decks for the player using the provided cardValuer function + */ public int getTotal(int playerId, Function cardValuer) { int score = playerHands[playerId].sumInt(cardValuer); score += playerDiscards[playerId].sumInt(cardValuer); @@ -349,6 +367,7 @@ public int getTotal(int playerId, Function cardValuer) { return score; } + // How many cards does the player have in total? public int getTotalCards(int playerId) { return playerDrawPiles[playerId].getSize() + playerDiscards[playerId].getSize() + playerHands[playerId].getSize() + playerTableaux[playerId].getSize(); diff --git a/src/main/java/games/dominion/DominionHeuristic.java b/src/main/java/games/dominion/DominionHeuristic.java index ca88be736..bbf96e9ff 100644 --- a/src/main/java/games/dominion/DominionHeuristic.java +++ b/src/main/java/games/dominion/DominionHeuristic.java @@ -99,12 +99,12 @@ public double evaluateState(AbstractGameState gs, int playerId) { // actionsLeft / 5. if (actionsLeft != 0.0) if (state.getCurrentPlayer() == playerId) - retValue += actionsLeft * Math.min(state.actionsLeft() / 5.0, 1.0); + retValue += actionsLeft * Math.min(state.getActionsLeft() / 5.0, 1.0); // buysLeft / 5 if (buysLeft != 0.0) if (state.getCurrentPlayer() == playerId) - retValue += buysLeft * Math.min(state.buysLeft() / 5.0, 1.0); + retValue += buysLeft * Math.min(state.getBuysLeft() / 5.0, 1.0); if (provinceCount != 0.0) retValue += provinceCount * state.getTotal(playerId, c -> c.cardType() == CardType.PROVINCE ? 1 : 0) / 12.0; diff --git a/src/main/java/games/dominion/actions/Artisan.java b/src/main/java/games/dominion/actions/Artisan.java index 31002f286..76cf24259 100644 --- a/src/main/java/games/dominion/actions/Artisan.java +++ b/src/main/java/games/dominion/actions/Artisan.java @@ -34,7 +34,7 @@ boolean _execute(DominionGameState state) { public List _computeAvailableActions(AbstractGameState gs) { DominionGameState state = (DominionGameState) gs; if (!gainedCard) { - return state.cardsToBuy().stream() + return state.getCardsToBuy().stream() .filter(c -> c.cost <= MAX_COST_OF_GAINED_CARD) .map(c -> new GainCard(c, player, DeckType.HAND)) .collect(toList()); diff --git a/src/main/java/games/dominion/actions/BuyCard.java b/src/main/java/games/dominion/actions/BuyCard.java index 699daee8e..58a4e18d3 100644 --- a/src/main/java/games/dominion/actions/BuyCard.java +++ b/src/main/java/games/dominion/actions/BuyCard.java @@ -27,7 +27,7 @@ public boolean execute(AbstractGameState gs) { // ii) Removing the card from the table and adding it to the player's discard pile // iii) Updating the available money and buy actions DominionGameState state = (DominionGameState) gs; - if (state.buysLeft() > 0 && state.availableSpend(buyingPlayer) >= cardType.cost) { + if (state.getBuysLeft() > 0 && state.getAvailableSpend(buyingPlayer) >= cardType.cost) { boolean success = super.execute(state); if (success) { state.changeBuys(-1); diff --git a/src/main/java/games/dominion/actions/DominionAction.java b/src/main/java/games/dominion/actions/DominionAction.java index a6dcca52a..2231652db 100644 --- a/src/main/java/games/dominion/actions/DominionAction.java +++ b/src/main/java/games/dominion/actions/DominionAction.java @@ -34,7 +34,7 @@ public boolean execute(AbstractGameState gs) { System.out.println(gs); throw new AssertionError("Attempting to play an action out of turn : " + this); } - if (!dummyAction && state.actionsLeft() < 1) { + if (!dummyAction && state.getActionsLeft() < 1) { System.out.println(gs); throw new AssertionError("Insufficient actions to play action card " + this); } diff --git a/src/main/java/games/dominion/actions/Mine.java b/src/main/java/games/dominion/actions/Mine.java index 376e909f1..d57fec83f 100644 --- a/src/main/java/games/dominion/actions/Mine.java +++ b/src/main/java/games/dominion/actions/Mine.java @@ -47,7 +47,7 @@ public List _computeAvailableActions(AbstractGameState gs) { .map(c -> new TrashCard(c.cardType(), player)) .distinct().collect(toList()); } else if (!gainedCard) { - retValue = state.cardsToBuy().stream() + retValue = state.getCardsToBuy().stream() .filter(c -> c.isTreasure && c.cost <= trashValue + BONUS_OVER_TRASHED_VALUE) .map(c -> new GainCard(c, player, DominionConstants.DeckType.HAND)) .collect(toList()); @@ -71,7 +71,7 @@ public void _afterAction(AbstractGameState gs, AbstractAction action) { if (!trashedCard && action instanceof TrashCard && ((TrashCard) action).player == player) { trashedCard = true; trashValue = ((TrashCard) action).trashedCard.cost; - if (state.cardsToBuy().stream().noneMatch(c -> c.isTreasure && c.cost <= trashValue + BONUS_OVER_TRASHED_VALUE)) + if (state.getCardsToBuy().stream().noneMatch(c -> c.isTreasure && c.cost <= trashValue + BONUS_OVER_TRASHED_VALUE)) gainedCard = true; // there are no valid cards to gain, so we skip the next decision // this is rare, but can happen if SILVER is exhausted with random players, say } diff --git a/src/main/java/games/dominion/actions/Remodel.java b/src/main/java/games/dominion/actions/Remodel.java index a000e486b..46b66e237 100644 --- a/src/main/java/games/dominion/actions/Remodel.java +++ b/src/main/java/games/dominion/actions/Remodel.java @@ -53,7 +53,7 @@ public List _computeAvailableActions(AbstractGameState gs) { } else { //Phase 2 - gain a card costing up to 2 more int budget = cardTrashed.cost + BONUS_OVER_TRASHED_VALUE; - retValue = state.cardsToBuy().stream() + retValue = state.getCardsToBuy().stream() .filter(ct -> ct.cost <= budget) .map(ct -> new GainCard(ct, player)) .collect(toList()); diff --git a/src/main/java/games/dominion/actions/Workshop.java b/src/main/java/games/dominion/actions/Workshop.java index 472ca8cba..51962724b 100644 --- a/src/main/java/games/dominion/actions/Workshop.java +++ b/src/main/java/games/dominion/actions/Workshop.java @@ -32,7 +32,7 @@ boolean _execute(DominionGameState state) { @Override public List _computeAvailableActions(AbstractGameState gs) { DominionGameState state = (DominionGameState) gs; - List retValue = state.cardsToBuy().stream() + List retValue = state.getCardsToBuy().stream() .filter(c -> c.cost <= COST_OF_GAINED_CARD) .map(c -> new GainCard(c, state.getCurrentPlayer())) .collect(toList()); diff --git a/src/main/java/games/dominion/gui/DominionPlayerView.java b/src/main/java/games/dominion/gui/DominionPlayerView.java index 0bd4a9b58..c0556ede1 100644 --- a/src/main/java/games/dominion/gui/DominionPlayerView.java +++ b/src/main/java/games/dominion/gui/DominionPlayerView.java @@ -78,9 +78,9 @@ public void update(DominionGameState state) { // There is no need to updateComponent on the DeckViews, as once initialised, the same Deck is retained throughout a Game // (if we ignore the copies of GameState used in MCTS etc.) if (state.getCurrentPlayer() == playerId) { - actions = state.actionsLeft(); - spendAvailable = state.availableSpend(playerId); - buys = state.buysLeft(); + actions = state.getActionsLeft(); + spendAvailable = state.getAvailableSpend(playerId); + buys = state.getBuysLeft(); } else { actions = 0; spendAvailable = 0; diff --git a/src/main/java/games/dominion/metrics/DomStateFeatures.java b/src/main/java/games/dominion/metrics/DomStateFeatures.java index 9fa74c927..4615588aa 100644 --- a/src/main/java/games/dominion/metrics/DomStateFeatures.java +++ b/src/main/java/games/dominion/metrics/DomStateFeatures.java @@ -58,11 +58,11 @@ public double[] localFeatureVector(AbstractGameState gs, int playerId) { // actionsLeft / 5. if (state.getCurrentPlayer() == playerId) - retValue[4] = state.actionsLeft() / 5.0; + retValue[4] = state.getActionsLeft() / 5.0; // buysLeft / 5 if (state.getCurrentPlayer() == playerId) - retValue[5] = state.buysLeft() / 5.0; + retValue[5] = state.getBuysLeft() / 5.0; // Total cards / 40 retValue[6] = state.getTotalCards(playerId) / 40.0; diff --git a/src/main/java/games/dominion/metrics/DomStateFeaturesReduced.java b/src/main/java/games/dominion/metrics/DomStateFeaturesReduced.java index 07b74dcec..3f5e73b52 100644 --- a/src/main/java/games/dominion/metrics/DomStateFeaturesReduced.java +++ b/src/main/java/games/dominion/metrics/DomStateFeaturesReduced.java @@ -1,7 +1,6 @@ package games.dominion.metrics; import core.AbstractGameState; -import core.CoreConstants; import core.interfaces.IStateFeatureVector; import games.dominion.DominionConstants; import games.dominion.DominionGameState; @@ -38,11 +37,11 @@ public double[] featureVector(AbstractGameState gs, int playerId) { // actionsLeft if (state.getCurrentPlayer() == playerId) - retValue[5] = state.actionsLeft(); + retValue[5] = state.getActionsLeft(); // buysLeft if (state.getCurrentPlayer() == playerId) - retValue[6] = state.buysLeft() / 5.0; + retValue[6] = state.getBuysLeft() / 5.0; retValue[7] = state.getTotal(playerId, c -> c.cardType() == CardType.PROVINCE ? 1 : 0); diff --git a/src/main/java/games/dominion/metrics/DominionMetrics.java b/src/main/java/games/dominion/metrics/DominionMetrics.java index a0725c2e4..4783e5f47 100644 --- a/src/main/java/games/dominion/metrics/DominionMetrics.java +++ b/src/main/java/games/dominion/metrics/DominionMetrics.java @@ -113,9 +113,9 @@ public Map> getColumns(int nPlayersPerGame, Set playerN @Override protected boolean _run(MetricsGameListener listener, Event e, Map records) { DominionGameState state = (DominionGameState)e.state; - records.put("Money", state.availableSpend(e.playerID)); - records.put("Actions", state.actionsLeft()); - records.put("Buys", state.buysLeft()); + records.put("Money", state.getAvailableSpend(e.playerID)); + records.put("Actions", state.getActionsLeft()); + records.put("Buys", state.getBuysLeft()); return true; } diff --git a/src/main/java/games/dominion/players/BigMoney.java b/src/main/java/games/dominion/players/BigMoney.java index 0d40c5d39..45c77066a 100644 --- a/src/main/java/games/dominion/players/BigMoney.java +++ b/src/main/java/games/dominion/players/BigMoney.java @@ -29,7 +29,7 @@ public AbstractAction _getAction(AbstractGameState gameState, List cardTypes = new ArrayList<>(Arrays.asList("EXPLODING_KITTEN", "DEFUSE", "NOPE", "ATTACK", "SKIP", "FAVOR", - "SHUFFLE", "SEETHEFUTURE", "TACOCAT", "MELONCAT", "FURRYCAT", "BEARDCAT", "RAINBOWCAT")); - private final int N_CARDS_TO_CHECK = 10; +public class ExplodingKittensForwardModel extends StandardForwardModel { - /** - * Performs initial game setup according to game rules. - * @param firstState - the state to be modified to the initial game state. - */ + + @Override protected void _setup(AbstractGameState firstState) { - ExplodingKittensGameState ekgs = (ExplodingKittensGameState)firstState; - ExplodingKittensParameters ekp = (ExplodingKittensParameters)firstState.getGameParameters(); + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) firstState; + ExplodingKittensParameters ekp = (ExplodingKittensParameters) firstState.getGameParameters(); ekgs.playerHandCards = new ArrayList<>(); - ekgs.playerGettingAFavor = -1; - ekgs.actionStack = null; // Set up draw pile deck - PartialObservableDeck drawPile = new PartialObservableDeck<>("Draw Pile", -1, firstState.getNPlayers(), VisibilityMode.HIDDEN_TO_ALL); - ekgs.setDrawPile(drawPile); + PartialObservableDeck drawPile = new PartialObservableDeck<>("Draw Pile", -1, firstState.getNPlayers(), CoreConstants.VisibilityMode.HIDDEN_TO_ALL); + ekgs.drawPile = drawPile; + ekgs.inPlay = new Deck<>("In Play", CoreConstants.VisibilityMode.VISIBLE_TO_ALL); // Add all cards but defuse and exploding kittens for (HashMap.Entry entry : ekp.cardCounts.entrySet()) { - if (entry.getKey() == ExplodingKittensCard.CardType.DEFUSE || entry.getKey() == ExplodingKittensCard.CardType.EXPLODING_KITTEN) + if (entry.getKey() == DEFUSE || entry.getKey() == EXPLODING_KITTEN) continue; for (int i = 0; i < entry.getValue(); i++) { ExplodingKittensCard card = new ExplodingKittensCard(entry.getKey()); drawPile.add(card); } } - ekgs.getDrawPile().shuffle(ekgs.getRnd()); + ekgs.drawPile.shuffle(ekgs.getRnd()); // Set up player hands List> playerHandCards = new ArrayList<>(firstState.getNPlayers()); @@ -62,435 +47,169 @@ protected void _setup(AbstractGameState firstState) { playerHandCards.add(playerCards); // Add defuse card - ExplodingKittensCard defuse = new ExplodingKittensCard(ExplodingKittensCard.CardType.DEFUSE); + ExplodingKittensCard defuse = new ExplodingKittensCard(DEFUSE); defuse.setOwnerId(i); playerCards.add(defuse); // Add N random cards from the deck for (int j = 0; j < ekp.nCardsPerPlayer; j++) { - ExplodingKittensCard c = ekgs.getDrawPile().draw(); + ExplodingKittensCard c = ekgs.drawPile.draw(); c.setOwnerId(i); playerCards.add(c); } } - ekgs.setPlayerHandCards(playerHandCards); - ekgs.setDiscardPile(new Deck<>("Discard Pile", VisibilityMode.VISIBLE_TO_ALL)); + ekgs.playerHandCards = playerHandCards; + ekgs.discardPile = new Deck<>("Discard Pile", CoreConstants.VisibilityMode.VISIBLE_TO_ALL); // Add remaining defuse cards and exploding kitten cards to the deck and shuffle again - for (int i = ekgs.getNPlayers(); i < ekp.nDefuseCards; i++){ - ExplodingKittensCard defuse = new ExplodingKittensCard(ExplodingKittensCard.CardType.DEFUSE); + for (int i = ekgs.getNPlayers(); i < ekp.nDefuseCards; i++) { + ExplodingKittensCard defuse = new ExplodingKittensCard(DEFUSE); drawPile.add(defuse); } - for (int i = 0; i < ekgs.getNPlayers() + ekp.cardCounts.get(ExplodingKittensCard.CardType.EXPLODING_KITTEN); i++){ - ExplodingKittensCard explodingKitten = new ExplodingKittensCard(ExplodingKittensCard.CardType.EXPLODING_KITTEN); + for (int i = 0; i < ekgs.getNPlayers() + ekp.cardCounts.get(EXPLODING_KITTEN); i++) { + ExplodingKittensCard explodingKitten = new ExplodingKittensCard(EXPLODING_KITTEN); drawPile.add(explodingKitten); } drawPile.shuffle(ekgs.getRnd()); - ekgs.setActionStack(new Stack<>()); ekgs.orderOfPlayerDeath = new int[ekgs.getNPlayers()]; + ekgs.currentPlayerTurnsLeft = 1; ekgs.setGamePhase(CoreConstants.DefaultGamePhase.Main); } - /** - * Applies the given action to the game state and executes any other game rules. - * @param gameState - current game state, to be modified by the action. - * @param action - action requested to be played by a player. - */ @Override - protected void _next(AbstractGameState gameState, AbstractAction action) { - ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; - ExplodingKittensTurnOrder ekTurnOrder = (ExplodingKittensTurnOrder) ekgs.getTurnOrder(); - Stack actionStack = ekgs.getActionStack(); + protected void _afterAction(AbstractGameState state, AbstractAction action) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) state; + if (ekgs.isActionInProgress()) + return; - if (action instanceof IsNopeable) { - actionStack.add(action); - ((IsNopeable) action).actionPlayed(ekgs); + // We may have some special statuses to be aware of: + // - If a player has skipped, they skip the draw card - ekTurnOrder.registerNopeableActionByPlayer(ekgs, ekTurnOrder.getCurrentPlayer(ekgs)); - if (action instanceof NopeAction) { - // Nope cards added immediately to avoid infinite nopeage - action.execute(ekgs); + // - If a player has been attacked, they will have to take two turns + // - This is stored in the currentAttackLevel, which has to decrement down to zero before play passes on + // - the nextAttackLevel indicates the attack level that will then be applied to the next player + + if (ekgs.isNotTerminal()) { + if (ekgs.skip) { + ekgs.setSkip(false); } else { - if (ekTurnOrder.reactionsFinished()){ - action.execute(ekgs); - actionStack.clear(); + // Draw a card for the current player + int currentPlayer = ekgs.getCurrentPlayer(); + ExplodingKittensCard card = ekgs.drawPile.draw(); + if (card == null) { + throw new AssertionError("No cards left in the draw pile"); } - } - } else if (action instanceof PassAction) { - - ekTurnOrder.endPlayerTurnStep(gameState); - - if (ekTurnOrder.reactionsFinished()) { - // apply stack - if (actionStack.size()%2 == 0){ - while (actionStack.size() > 1) { - actionStack.pop(); + if (card.cardType == EXPLODING_KITTEN) { + if (ekgs.playerHandCards.get(currentPlayer).stream().anyMatch(c -> c.cardType == DEFUSE)) { + // Exploding kitten drawn, player has defuse + ExplodingKittensCard defuseCard = ekgs.playerHandCards.get(currentPlayer).stream().filter(c -> c.cardType == DEFUSE).findFirst().get(); + ekgs.playerHandCards.get(currentPlayer).remove(defuseCard); + ekgs.playerHandCards.get(currentPlayer).add(card); // add Exploding Kitten (to be removed) + ekgs.discardPile.add(defuseCard); + ekgs.setActionInProgress(new DefuseKitten(currentPlayer)); + return; + } else { + // Exploding kitten drawn, player is dead + if (ekgs.getPlayerResults()[currentPlayer] == CoreConstants.GameResult.LOSE_GAME) { + throw new AssertionError("Player " + currentPlayer + " is already dead"); + } + ekgs.discardPile.add(card); + killPlayer(ekgs, currentPlayer); + ekgs.currentPlayerTurnsLeft = 1; // force end of player turn later } - //Action was successfully noped - ((IsNopeable) actionStack.pop()).nopedExecute(gameState); -// if (gameState.getCoreGameParameters().verbose) { -// System.out.println("Action was successfully noped"); -// } } else { -// if (actionStack.size() > 2 && gameState.getCoreGameParameters().verbose) { -// System.out.println("All nopes were noped"); -// } - - while (actionStack.size() > 1) { - actionStack.pop(); - } - - //Action can be played - AbstractAction stackedAction = actionStack.get(0); - stackedAction.execute(gameState); - } - actionStack.clear(); - if (ekgs.getGamePhase() == Nope) { - ekgs.setGamePhase(CoreConstants.DefaultGamePhase.Main); + card.setOwnerId(currentPlayer); + ekgs.playerHandCards.get(currentPlayer).add(card); } } - } else { - action.execute(gameState); + ekgs.currentPlayerTurnsLeft--; + if (ekgs.currentPlayerTurnsLeft == 0 && ekgs.isNotTerminal()) { + endPlayerTurn(ekgs); + ekgs.currentPlayerTurnsLeft = Math.max(1, ekgs.nextAttackLevel); + ekgs.nextAttackLevel = 0; + } } } - @Override - protected List _computeAvailableActions(AbstractGameState gameState) { - return _computeAvailableActions(gameState, ActionSpace.Default); - } - - /** - * Calculates the list of currently available actions, possibly depending on the game phase. - * @return - List of AbstractAction objects. - */ - @Override - protected List _computeAvailableActions(AbstractGameState gameState, ActionSpace actionSpace) { - ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; - ArrayList actions; - // The actions per player do not change a lot in between two turns - // Could update an existing list instead of generating a new list every time we query this function + public void killPlayer(ExplodingKittensGameState ekgs, int playerID) { + ekgs.setPlayerResult(CoreConstants.GameResult.LOSE_GAME, playerID); + ekgs.logEvent(new IGameEvent() { + @Override + public String name() { + return "Player " + playerID + " has been killed"; + } - // Find actions for the player depending on current game phase - int player = ekgs.getCurrentPlayer(); - if (CoreConstants.DefaultGamePhase.Main.equals(ekgs.getGamePhase())) { - actions = playerActions(ekgs, player); - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Defuse.equals(ekgs.getGamePhase())) { - actions = placeKittenActions(ekgs, player); - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Nope.equals(ekgs.getGamePhase())) { - actions = nopeActions(ekgs, player); - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Favor.equals(ekgs.getGamePhase())) { - actions = favorActions(ekgs, player); - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.SeeTheFuture.equals(ekgs.getGamePhase())) { - actions = seeTheFutureActions(ekgs, player); - } else { - actions = new ArrayList<>(); + @Override + public Set getValues() { + return Set.of(); + } + }); + int players = ekgs.getNPlayers(); + int nPlayersActive = 0; + for (int i = 0; i < players; i++) { + if (ekgs.getPlayerResults()[i] == CoreConstants.GameResult.GAME_ONGOING) nPlayersActive++; + } + ekgs.orderOfPlayerDeath[playerID] = players - nPlayersActive; + if (nPlayersActive == 1) { + endGame(ekgs); } - - return actions; } - @Override - protected AbstractForwardModel _copy() { - return new ExplodingKittensForwardModel(); - } + /** + * Calculates the list of currently available actions, possibly depending on the game phase. + * + * @return - List of AbstractAction objects. + */ @Override - protected void endPlayerTurn(AbstractGameState state) { - ExplodingKittensGameState ekgs = (ExplodingKittensGameState) state; - ekgs.getTurnOrder().endPlayerTurn(ekgs); - } + protected List _computeAvailableActions(AbstractGameState gameState) { - private ArrayList playerActions(ExplodingKittensGameState ekgs, int playerID){ - ArrayList actions = new ArrayList<>(); + // This is called when it is a player's main turn; not during the Nope interrupts + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; + int playerID = ekgs.getCurrentPlayer(); + List actions = new ArrayList<>(); Deck playerDeck = ekgs.playerHandCards.get(playerID); -// ActionTreeNode playNode = root.findChildrenByName("PLAY"); - HashSet types = new HashSet<>(); - for (int c = 0; c < playerDeck.getSize(); c++) { - ExplodingKittensCard card = playerDeck.get(c); - if (types.contains(card.cardType)) continue; - types.add(card.cardType); + // We can pass, or play any card in our hand + actions.add(new Pass()); + // get all unique playable types in hand + Set playableTypes = new HashSet<>(); + for (int i = 0; i < playerDeck.getSize(); i++) { + playableTypes.add(playerDeck.get(i).cardType); + } + // remove defuse and exploding kittens from playable types + playableTypes.remove(DEFUSE); + playableTypes.remove(EXPLODING_KITTEN); + playableTypes.remove(NOPE); - switch (card.cardType) { - case DEFUSE: - case MELONCAT: - case RAINBOWCAT: - case FURRYCAT: - case BEARDCAT: - case TACOCAT: - case NOPE: - case EXPLODING_KITTEN: - break; - case SKIP: -// playNode.setValue(1); - actions.add(new SkipAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); -// playNode.findChildrenByName("SKIP").setAction(new SkipAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); - break; + for (ExplodingKittensCard.CardType type : playableTypes) { + switch (type) { case FAVOR: - for (int player = 0; player < ekgs.getNPlayers(); player++) { - if (player == playerID) - continue; - if (ekgs.playerHandCards.get(player).getSize() > 0) { - actions.add(new FavorAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, player)); -// playNode.setValue(1); -// playNode.findChildrenByName("FAVOR").setAction(new FavorAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, player)); + for (int i = 0; i < ekgs.getNPlayers(); i++) { + if (i != playerID) { + if (ekgs.isNotTerminalForPlayer(i) && ekgs.getPlayerHand(i).getSize() > 0) + actions.add(new PlayEKCard(FAVOR, i)); } } break; - case ATTACK: - for (int targetPlayer = 0; targetPlayer < ekgs.getNPlayers(); targetPlayer++) { - - if (targetPlayer == playerID || ekgs.getPlayerResults()[targetPlayer] != CoreConstants.GameResult.GAME_ONGOING) - continue; - - actions.add(new AttackAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, targetPlayer)); -// playNode.setValue(1); -// playNode.findChildrenByName("ATTACK").setAction(new AttackAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, targetPlayer)); - } - break; - case SHUFFLE: - actions.add(new ShuffleAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); -// playNode.setValue(1); -// playNode.findChildrenByName("SHUFFLE").setAction(new ShuffleAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); - break; - case SEETHEFUTURE: -// actions.add(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); -// playNode.setValue(1); -// playNode.findChildrenByName("SEETHEFUTURE").setAction(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); + case SHUFFLE, SKIP, ATTACK, SEETHEFUTURE: + actions.add(new PlayEKCard(type)); break; default: - System.out.println("No actions known for cardtype: " + card.cardType); - } - } - /* todo add special combos - // can take any card from anyone - for (int i = 0; i < nPlayers; i++){ - if (i != activePlayer){ - Deck otherDeck = (Deck)this.areas.get(activePlayer).getComponent(playerHandHash); - for (Card card: otherDeck.getCards()){ - core.actions.add(new TakeCard(card, i)); - } - } - }*/ - - // add end turn by drawing a card - actions.add(new DrawExplodingKittenCard(ekgs.drawPile.getComponentID(), playerDeck.getComponentID())); -// root.findChildrenByName("DRAW").setAction(new DrawExplodingKittenCard(ekgs.drawPile.getComponentID(), playerDeck.getComponentID())); - return actions; - } - - private ArrayList placeKittenActions(ExplodingKittensGameState ekgs, int playerID){ - ArrayList actions = new ArrayList<>(); - Deck playerDeck = ekgs.playerHandCards.get(playerID); - int explodingKittenCard = -1; - for (int i = 0; i < playerDeck.getSize(); i++) { - if (playerDeck.getComponents().get(i).cardType == ExplodingKittensCard.CardType.EXPLODING_KITTEN) { - explodingKittenCard = i; - break; - } - } - if (explodingKittenCard != -1) { - for (int i = 0; i <= ekgs.drawPile.getSize(); i++) { - actions.add(new PlaceExplodingKitten(playerDeck.getComponentID(), ekgs.drawPile.getComponentID(), explodingKittenCard, i)); -// if (i < N_CARDS_TO_CHECK){ -// root.findChildrenByName("DEFUSE").findChildrenByName("PUT" + i, true).setAction(new PlaceExplodingKitten(playerDeck.getComponentID(), ekgs.drawPile.getComponentID(), explodingKittenCard, i)); -// } - } - } - return actions; - } - - private ArrayList nopeActions(ExplodingKittensGameState ekgs, int playerID){ - ArrayList actions = new ArrayList<>(); - Deck playerDeck = ekgs.playerHandCards.get(playerID); - for (int c = 0; c < playerDeck.getSize(); c++) { - if (playerDeck.getComponents().get(c).cardType == ExplodingKittensCard.CardType.NOPE) { - actions.add(new NopeAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); -// root.findChildrenByName("NOPE").findChildrenByName("NOPE").setAction(new NopeAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); - break; - } - } -// root.findChildrenByName("NOPE").findChildrenByName("PASS").setAction(new PassAction()); - actions.add(new PassAction()); - return actions; - } - - private ArrayList favorActions(ExplodingKittensGameState ekgs, int playerID){ - ArrayList actions = new ArrayList<>(); - Deck playerDeck = ekgs.playerHandCards.get(playerID); - Deck receiverDeck = ekgs.playerHandCards.get(ekgs.playerGettingAFavor); - // todo this requires mapping the card type to indices - for (int card = 0; card < playerDeck.getSize(); card++) { - actions.add(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), card)); -// root.findChildrenByName("FAVOR").findChildrenByName(playerDeck.get(card).toString()).setAction(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), card)); - } - if (actions.isEmpty()) // the target has no cards. - actions.add(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), -1)); - return actions; - } - - private ArrayList seeTheFutureActions(ExplodingKittensGameState ekgs, int playerID){ - ArrayList actions = new ArrayList<>(); - Deck playerDeck = ekgs.playerHandCards.get(playerID); - - int cardIdx = -1; - for (int c = 0; c < playerDeck.getSize(); c++) { - if (playerDeck.get(c).cardType == ExplodingKittensCard.CardType.SEETHEFUTURE) { - cardIdx = c; - break; - } - } - - if (cardIdx != -1) { - int numberOfCards = ekgs.drawPile.getSize(); - int n = Math.min(((ExplodingKittensParameters) ekgs.getGameParameters()).nSeeFutureCards, numberOfCards); - if (n > 0) { - - ArrayList permutations = new ArrayList<>(); - int[] order = new int[n]; - for (int i = 0; i < n; i++) { - order[i] = i; - } - generatePermutations(n, order, permutations); - for (int[] perm : permutations) { - actions.add(new ChooseSeeTheFutureOrder(playerDeck.getComponentID(), - ekgs.discardPile.getComponentID(), cardIdx, ekgs.drawPile.getComponentID(), perm)); - } - } - } else { - throw new AssertionError("Player " + playerID + " does not have a SeeTheFuture card in hand."); - } - - return actions; - } - - @Override - public ActionTreeNode updateActionTree(ActionTreeNode root, AbstractGameState gameState) { - root.resetTree(); - ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; - - int playerID = ekgs.getCurrentPlayer(); - Deck playerDeck = ekgs.playerHandCards.get(playerID); - if (CoreConstants.DefaultGamePhase.Main.equals(ekgs.getGamePhase())) { - - ActionTreeNode playNode = root.findChildrenByName("PLAY"); - - HashSet types = new HashSet<>(); - for (int c = 0; c < playerDeck.getSize(); c++) { - ExplodingKittensCard card = playerDeck.get(c); - if (types.contains(card.cardType)) continue; - types.add(card.cardType); - - switch (card.cardType) { - case SKIP: - playNode.setValue(1); - playNode.findChildrenByName("SKIP").setAction(new SkipAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); - break; - case FAVOR: - for (int player = 0; player < ekgs.getNPlayers(); player++) { - if (player == playerID) - continue; - if (ekgs.playerHandCards.get(player).getSize() > 0) { - playNode.setValue(1); - playNode.findChildrenByName("FAVOR").setAction(new FavorAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, player)); - } + // for Cat Cards we need a pair + for (int i = 0; i < ekgs.getNPlayers(); i++) { + if (i != playerID) { + if (ekgs.isNotTerminalForPlayer(i) && ekgs.getPlayerHand(i).getSize() > 0) + if (ekgs.playerHandCards.get(playerID).stream().filter(c -> c.cardType == type).count() > 1) + actions.add(new PlayEKCard(type, i)); } - break; - case ATTACK: - for (int targetPlayer = 0; targetPlayer < ekgs.getNPlayers(); targetPlayer++) { - - if (targetPlayer == playerID || ekgs.getPlayerResults()[targetPlayer] != CoreConstants.GameResult.GAME_ONGOING) - continue; - - playNode.setValue(1); - playNode.findChildrenByName("ATTACK").setAction(new AttackAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, targetPlayer)); - } - break; - case SHUFFLE: - playNode.setValue(1); - playNode.findChildrenByName("SHUFFLE").setAction(new ShuffleAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); - break; - case SEETHEFUTURE: - // todo this action not implemented correctly - // actions.add(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); -// playNode.setValue(1); - // playNode.findChildrenByName("SEETHEFUTURE").setAction(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); - break; -// default: -// System.out.println("No actions known for cardtype: " + card.cardType); - } - } - - // add end turn by drawing a card - root.findChildrenByName("DRAW").setAction(new DrawExplodingKittenCard(ekgs.drawPile.getComponentID(), playerDeck.getComponentID())); - - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Defuse.equals(ekgs.getGamePhase())) { - int explodingKittenCard = -1; - for (int i = 0; i < playerDeck.getSize(); i++) { - if (playerDeck.getComponents().get(i).cardType == ExplodingKittensCard.CardType.EXPLODING_KITTEN) { - explodingKittenCard = i; - break; - } - } - if (explodingKittenCard != -1) { - for (int i = 0; i <= ekgs.drawPile.getSize(); i++) { - if (i < N_CARDS_TO_CHECK){ - root.findChildrenByName("DEFUSE").findChildrenByName("PUT" + i, true).setAction(new PlaceExplodingKitten(playerDeck.getComponentID(), ekgs.drawPile.getComponentID(), explodingKittenCard, i)); } - } - } - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Nope.equals(ekgs.getGamePhase())) { - for (int c = 0; c < playerDeck.getSize(); c++) { - if (playerDeck.getComponents().get(c).cardType == ExplodingKittensCard.CardType.NOPE) { - root.findChildrenByName("NOPE").findChildrenByName("NOPE").setAction(new NopeAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); - break; - } } - root.findChildrenByName("NOPE").findChildrenByName("PASS").setAction(new PassAction()); - System.out.println("got boht nope actions"); - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Favor.equals(ekgs.getGamePhase())) { - Deck receiverDeck = ekgs.playerHandCards.get(ekgs.playerGettingAFavor); - // this requires mapping the card type to indices - for (int card = 0; card < playerDeck.getSize(); card++) { - root.findChildrenByName("FAVOR").findChildrenByName(playerDeck.get(card).toString()).setAction(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), card)); - } - } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.SeeTheFuture.equals(ekgs.getGamePhase())) { - // todo this is not correctly implemented } - - return root; + return actions; } - public ActionTreeNode initActionTree(AbstractGameState gs){ - // set up the action tree - ActionTreeNode tree = new ActionTreeNode(0, "root"); - tree.addChild(0, "DRAW"); - ActionTreeNode play = tree.addChild(0, "PLAY"); - ActionTreeNode favor = tree.addChild(0, "FAVOR"); - ActionTreeNode nope = tree.addChild(0, "NOPE"); - ActionTreeNode defuse = tree.addChild(0, "DEFUSE"); - for (String cardType : cardTypes) { - ActionTreeNode cardTypeNode = play.addChild(0, cardType); - // can ask a favor from any other player - if (cardType.equals("FAVOR")) { - for (int j = 0; j < GameType.ExplodingKittens.getMaxPlayers(); j++) { - cardTypeNode.addChild(0, "PLAYER" + j); - } - } - favor.addChild(0, cardType); - } - // if player has a nope card it can decide if it wants to play it or not - nope.addChild(0, "NOPE"); - nope.addChild(0, "PASS"); - - // allow to put the back the defuse card to these positions - for (int i =0; i < N_CARDS_TO_CHECK; i++){ - defuse.addChild(0, "PUT"+i); - - } - return tree; - } } diff --git a/src/main/java/games/explodingkittens/ExplodingKittensGameState.java b/src/main/java/games/explodingkittens/ExplodingKittensGameState.java index 5faf9857e..d010f28c4 100644 --- a/src/main/java/games/explodingkittens/ExplodingKittensGameState.java +++ b/src/main/java/games/explodingkittens/ExplodingKittensGameState.java @@ -1,23 +1,20 @@ package games.explodingkittens; -import core.AbstractGameStateWithTurnOrder; +import core.AbstractGameState; import core.AbstractParameters; import core.CoreConstants; -import core.actions.AbstractAction; import core.components.Component; import core.components.Deck; import core.components.PartialObservableDeck; -import core.interfaces.IGamePhase; -import core.interfaces.IPrintable; -import core.turnorders.TurnOrder; import games.GameType; import games.explodingkittens.cards.ExplodingKittensCard; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; -import static core.CoreConstants.VisibilityMode; - -public class ExplodingKittensGameState extends AbstractGameStateWithTurnOrder implements IPrintable { +public class ExplodingKittensGameState extends AbstractGameState { // Cards in each player's hand, index corresponds to player ID List> playerHandCards; @@ -25,19 +22,15 @@ public class ExplodingKittensGameState extends AbstractGameStateWithTurnOrder im PartialObservableDeck drawPile; // Cards in the discard pile Deck discardPile; - // Player ID of the player currently getting a favor - int playerGettingAFavor; - // Current stack of actions - Stack actionStack; + Deck inPlay; + int currentPlayerTurnsLeft = 0; + int nextAttackLevel = 0; + boolean skip = false; + int[] orderOfPlayerDeath; + public ExplodingKittensGameState(AbstractParameters gameParameters, int nPlayers) { super(gameParameters, nPlayers); - playerGettingAFavor = -1; - playerHandCards = new ArrayList<>(); - } - @Override - protected TurnOrder _createTurnOrder(int nPlayers) { - return new ExplodingKittensTurnOrder(nPlayers); } @Override @@ -47,22 +40,30 @@ protected GameType _getGameType() { @Override protected List _getAllComponents() { - return new ArrayList() {{ - add(drawPile); - add(discardPile); - addAll(playerHandCards); - }}; + List ret = new ArrayList<>(); + ret.add(drawPile); + ret.add(discardPile); + ret.addAll(playerHandCards); + return ret; + } + + // when a card is played to the table, but before + public void setInPlay(ExplodingKittensCard.CardType cardType, int playerID) { + ExplodingKittensCard card = playerHandCards.get(playerID).stream() + .filter(c -> c.cardType == cardType) + .findFirst() + .orElseThrow(() -> new AssertionError("Player " + playerID + " does not have card " + cardType + " to play")); + inPlay.add(card); + playerHandCards.get(playerID).remove(card); } @Override - protected AbstractGameStateWithTurnOrder __copy(int playerId) { + protected ExplodingKittensGameState _copy(int playerId) { ExplodingKittensGameState ekgs = new ExplodingKittensGameState(gameParameters.copy(), getNPlayers()); ekgs.discardPile = discardPile.copy(); - ekgs.playerGettingAFavor = playerGettingAFavor; - ekgs.actionStack = new Stack<>(); - for (AbstractAction a : actionStack) { - ekgs.actionStack.add(a.copy()); - } + ekgs.currentPlayerTurnsLeft = currentPlayerTurnsLeft; + ekgs.nextAttackLevel = nextAttackLevel; + ekgs.inPlay = inPlay.copy(); ekgs.orderOfPlayerDeath = orderOfPlayerDeath.clone(); ekgs.playerHandCards = new ArrayList<>(); for (PartialObservableDeck d : playerHandCards) { @@ -92,7 +93,7 @@ protected AbstractGameStateWithTurnOrder __copy(int playerId) { // Shuffles only hidden cards in draw pile, if player knows what's on top those will stay in place ekgs.drawPile.redeterminiseUnknown(redeterminisationRnd, playerId); - Deck explosive = new Deck<>("tmp", VisibilityMode.HIDDEN_TO_ALL); + Deck explosive = new Deck<>("tmp", CoreConstants.VisibilityMode.HIDDEN_TO_ALL); for (int i = 0; i < getNPlayers(); i++) { if (i != playerId) { for (int j = 0; j < playerHandCards.get(i).getSize(); j++) { @@ -122,19 +123,28 @@ protected AbstractGameStateWithTurnOrder __copy(int playerId) { return ekgs; } + public void setNextAttackLevel(int value) { + nextAttackLevel = value; + } + + public int getCurrentPlayerTurnsLeft() { + return currentPlayerTurnsLeft; + } + public void setCurrentPlayerTurnsLeft(int value) { + currentPlayerTurnsLeft = value; + } + + public boolean skipNext() { + return skip; + } + public void setSkip(boolean skip) { + this.skip = skip; + } @Override protected double _getHeuristicScore(int playerId) { return new ExplodingKittensHeuristic().evaluateState(this, playerId); } - /** - * This provides the current score in game turns. This will only be relevant for games that have the concept - * of victory points, etc. - * If a game does not support this directly, then just return 0.0 - * - * @param playerId - * @return - double, score of current state - */ @Override public double getGameScore(int playerId) { if (playerResults[playerId] == CoreConstants.GameResult.LOSE_GAME) @@ -144,147 +154,37 @@ public double getGameScore(int playerId) { return Arrays.stream(playerResults).filter(status -> status == CoreConstants.GameResult.LOSE_GAME).count() + 1; } - @Override - public int getOrdinalPosition(int playerId) { - if (playerResults[playerId] == CoreConstants.GameResult.WIN_GAME) - return 1; - if (playerResults[playerId] == CoreConstants.GameResult.LOSE_GAME) - return getNPlayers() - orderOfPlayerDeath[playerId] + 1; - return 1; // anyone still alive is jointly winning - } - - @Override - protected boolean _equals(Object o) { - if (this == o) return true; - if (!(o instanceof ExplodingKittensGameState)) return false; - if (!super.equals(o)) return false; - ExplodingKittensGameState gameState = (ExplodingKittensGameState) o; - return playerGettingAFavor == gameState.playerGettingAFavor && - Objects.equals(playerHandCards, gameState.playerHandCards) && - Objects.equals(drawPile, gameState.drawPile) && - Objects.equals(discardPile, gameState.discardPile) && - Objects.equals(actionStack, gameState.actionStack); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), playerHandCards, drawPile, discardPile, playerGettingAFavor, actionStack); - } - - /** - * Marks a player as dead. - * - * @param playerID - player who was killed in a kitten explosion. - */ - public void killPlayer(int playerID) { - setPlayerResult(CoreConstants.GameResult.LOSE_GAME, playerID); - int nPlayersActive = 0; - for (int i = 0; i < getNPlayers(); i++) { - if (playerResults[i] == CoreConstants.GameResult.GAME_ONGOING) nPlayersActive++; - } - orderOfPlayerDeath[playerID] = getNPlayers() - nPlayersActive; - if (nPlayersActive == 1) { - this.gameStatus = CoreConstants.GameResult.GAME_END; - for (int i = 0; i < getNPlayers(); i++) { - // set winner - if (playerResults[i] == CoreConstants.GameResult.GAME_ONGOING) { - playerResults[i] = CoreConstants.GameResult.WIN_GAME; - } - } - turnOrder.endGame(this); - } - - ((ExplodingKittensTurnOrder) getTurnOrder()).endPlayerTurnStep(this); - - } - - // Getters, setters - public int getPlayerGettingAFavor() { - return playerGettingAFavor; - } - public void setPlayerGettingAFavor(int playerGettingAFavor) { - this.playerGettingAFavor = playerGettingAFavor; + public PartialObservableDeck getPlayerHand(int playerId) { + return playerHandCards.get(playerId); } - public PartialObservableDeck getDrawPile() { return drawPile; } - - protected void setDrawPile(PartialObservableDeck drawPile) { - this.drawPile = drawPile; - } - public Deck getDiscardPile() { return discardPile; } - - // Protected, only accessible in this package and subclasses - protected void setDiscardPile(Deck discardPile) { - this.discardPile = discardPile; - } - - public Stack getActionStack() { - return actionStack; - } - - protected void setActionStack(Stack actionStack) { - this.actionStack = actionStack; - } - - public List> getPlayerHandCards() { - return playerHandCards; - } - - protected void setPlayerHandCards(List> playerHandCards) { - this.playerHandCards = playerHandCards; + public Deck getInPlay() { + return inPlay; } - public void printToConsole() { - System.out.println(toString()); + @Override + protected boolean _equals(Object o) { + if (o instanceof ExplodingKittensGameState other) { + return discardPile.equals(other.discardPile) && + Arrays.equals(orderOfPlayerDeath, other.orderOfPlayerDeath) && + playerHandCards.equals(other.playerHandCards) && + currentPlayerTurnsLeft == other.currentPlayerTurnsLeft && + nextAttackLevel == other.nextAttackLevel && + inPlay.equals(other.inPlay) && + drawPile.equals(other.drawPile); + }; + return false; } - - // Printing functions for the game state and decks. - @Override - public String toString() { - String s = "============================\n"; - - int currentPlayer = turnOrder.getCurrentPlayer(this); - - for (int i = 0; i < getNPlayers(); i++) { - if (currentPlayer == i) - s += ">>> Player " + i + ":"; - else - s += "Player " + i + ":"; - s += playerHandCards.get(i).toString() + "\n"; - } - - s += "\nDrawPile: "; - s += drawPile.toString() + "\n"; - - s += "DiscardPile: "; - s += discardPile.toString() + "\n"; - - s += "Action stack: "; - for (AbstractAction a : actionStack) { - s += a.toString() + ","; - } - s = s.substring(0, s.length() - 1); - s += "\n\n"; - - s += "Current GamePhase: " + gamePhase + "\n"; - s += "Missing Draws: " + ((ExplodingKittensTurnOrder) turnOrder).requiredDraws + "\n"; - s += "============================\n"; - return s; + public int hashCode() { + return Objects.hash(discardPile, playerHandCards, drawPile, inPlay, nextAttackLevel, currentPlayerTurnsLeft) + 31 * Arrays.hashCode(orderOfPlayerDeath); } - // Exploding kittens adds 4 phases on top of default ones. - public enum ExplodingKittensGamePhase implements IGamePhase { - Nope, - Defuse, - Favor, - SeeTheFuture - } } diff --git a/src/main/java/games/explodingkittens/ExplodingKittensHeuristic.java b/src/main/java/games/explodingkittens/ExplodingKittensHeuristic.java index 180d69ba6..9054e7df2 100644 --- a/src/main/java/games/explodingkittens/ExplodingKittensHeuristic.java +++ b/src/main/java/games/explodingkittens/ExplodingKittensHeuristic.java @@ -4,7 +4,6 @@ import core.CoreConstants; import core.interfaces.IStateHeuristic; import evaluation.optimisation.TunableParameters; -import games.explodingkittens.actions.IsNopeable; import games.explodingkittens.cards.ExplodingKittensCard; public class ExplodingKittensHeuristic extends TunableParameters implements IStateHeuristic { @@ -62,7 +61,6 @@ public double evaluateState(AbstractGameState gs, int playerId) { return cardValues / (ekgs.playerHandCards.get(playerId).getSize() + 1); } - // TODO: check state more double getCardValue(ExplodingKittensGameState ekgs, ExplodingKittensCard card) { switch (card.cardType) { case EXPLODING_KITTEN: @@ -70,7 +68,7 @@ public double evaluateState(AbstractGameState gs, int playerId) { case DEFUSE: return defuseValue; case NOPE: - if (ekgs.actionStack.size() > 0 && ekgs.actionStack.get(0) instanceof IsNopeable) { + if (ekgs.isActionInProgress()) { return nopeValue; } else return 0; // Neutral case ATTACK: @@ -82,49 +80,22 @@ public double evaluateState(AbstractGameState gs, int playerId) { case SHUFFLE: return shuffleValue; case SEETHEFUTURE: - return seeFutureValue; // TODO: higher if future not already known, otherwise low + return seeFutureValue; default: return regularValue; } } - /** - * Return a copy of this game parameters object, with the same parameters as in the original. - * - * @return - new game parameters object. - */ @Override protected ExplodingKittensHeuristic _copy() { - ExplodingKittensHeuristic retValue = new ExplodingKittensHeuristic(); - retValue.explodingValue = explodingValue; - retValue.defuseValue = defuseValue; - retValue.regularValue = regularValue; - retValue.seeFutureValue = seeFutureValue; - retValue.nopeValue = nopeValue; - retValue.attackValue = attackValue; - retValue.skipValue = skipValue; - retValue.favorValue = favorValue; - retValue.shuffleValue = shuffleValue; - return retValue; + return new ExplodingKittensHeuristic(); + // copying of parameterisable values is done in the super class } - /** - * Checks if the given object is the same as the current. - * - * @param o - other object to test equals for. - * @return true if the two objects are equal, false otherwise - */ @Override protected boolean _equals(Object o) { - if (o instanceof ExplodingKittensHeuristic) { - ExplodingKittensHeuristic other = (ExplodingKittensHeuristic) o; - return other.explodingValue == explodingValue && other.defuseValue == defuseValue && - other.regularValue == regularValue && other.seeFutureValue == seeFutureValue && - other.nopeValue == nopeValue && other.attackValue == attackValue && - other.skipValue == skipValue && other.favorValue == favorValue && - other.shuffleValue == shuffleValue; - } - return false; + return o instanceof ExplodingKittensHeuristic; + // checking of parameterisable values is done in the super class } /** @@ -133,7 +104,7 @@ protected boolean _equals(Object o) { */ @Override public ExplodingKittensHeuristic instantiate() { - return this._copy(); + return (ExplodingKittensHeuristic) this.copy(); } diff --git a/src/main/java/games/explodingkittens/ExplodingKittensParameters.java b/src/main/java/games/explodingkittens/ExplodingKittensParameters.java index 59895cb9a..eef76f8ed 100644 --- a/src/main/java/games/explodingkittens/ExplodingKittensParameters.java +++ b/src/main/java/games/explodingkittens/ExplodingKittensParameters.java @@ -6,15 +6,13 @@ import games.GameType; import games.explodingkittens.cards.ExplodingKittensCard; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Objects; +import java.util.*; public class ExplodingKittensParameters extends TunableParameters { String dataPath = "data/explodingkittens/"; - HashMap cardCounts = new HashMap() {{ + Map cardCounts = new HashMap<>() {{ put(ExplodingKittensCard.CardType.ATTACK, 4); put(ExplodingKittensCard.CardType.SKIP, 4); put(ExplodingKittensCard.CardType.FAVOR, 4); @@ -43,6 +41,7 @@ public ExplodingKittensParameters() { if (c == ExplodingKittensCard.CardType.EXPLODING_KITTEN) addTunableParameter(c.name() + " count", -1); else addTunableParameter(c.name() + " count", cardCounts.get(c), Arrays.asList(1,2,3,4,5)); } + addTunableParameter("dataPath", "data/explodingkittens/"); _reset(); } @@ -53,6 +52,7 @@ public void _reset() { nSeeFutureCards = (int) getParameterValue("nSeeFutureCards"); nopeOwnCards = (boolean) getParameterValue("nopeOwnCards"); cardCounts.replaceAll((c, v) -> (Integer) getParameterValue(c.name() + " count")); + dataPath = (String) getParameterValue("dataPath"); } public String getDataPath() { @@ -61,25 +61,14 @@ public String getDataPath() { @Override protected AbstractParameters _copy() { - return new ExplodingKittensParameters(); + ExplodingKittensParameters ekp = new ExplodingKittensParameters(); + ekp.cardCounts = new HashMap<>(cardCounts); + return ekp; } @Override protected boolean _equals(Object o) { - if (this == o) return true; - if (!(o instanceof ExplodingKittensParameters)) return false; - if (!super.equals(o)) return false; - ExplodingKittensParameters that = (ExplodingKittensParameters) o; - return nCardsPerPlayer == that.nCardsPerPlayer && - nDefuseCards == that.nDefuseCards && - nSeeFutureCards == that.nSeeFutureCards && - Objects.equals(dataPath, that.dataPath) && - Objects.equals(cardCounts, that.cardCounts); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), dataPath, cardCounts, nCardsPerPlayer, nDefuseCards, nSeeFutureCards); + return o instanceof ExplodingKittensParameters; } @Override diff --git a/src/main/java/games/explodingkittens/actions/ChoiceOfCardToGive.java b/src/main/java/games/explodingkittens/actions/ChoiceOfCardToGive.java new file mode 100644 index 000000000..35f7c9f3a --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/ChoiceOfCardToGive.java @@ -0,0 +1,67 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import core.interfaces.IExtendedSequence; +import games.explodingkittens.ExplodingKittensGameState; + +import java.util.List; +import java.util.stream.Collectors; + +public class ChoiceOfCardToGive implements IExtendedSequence { + + public final int giver; + public final int recipient; + public boolean executed; + + public ChoiceOfCardToGive(int giver, int recipient) { + this.giver = giver; + this.recipient = recipient; + } + + @Override + public List _computeAvailableActions(AbstractGameState gs) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + // consider all cards in hand + List actions = state.getPlayerHand(giver).stream() + .map(c -> new GiveCard(giver, recipient, c.cardType)) + .toList(); + return actions.stream().distinct().collect(Collectors.toList()); + } + + @Override + public int getCurrentPlayer(AbstractGameState state) { + return giver; + } + + @Override + public void _afterAction(AbstractGameState state, AbstractAction action) { + if (action instanceof GiveCard) + executed = true; + } + + @Override + public boolean executionComplete(AbstractGameState state) { + return executed; + } + + @Override + public ChoiceOfCardToGive copy() { + ChoiceOfCardToGive retValue = new ChoiceOfCardToGive(giver, recipient); + retValue.executed = executed; + return retValue; + } + + @Override + public String toString() { + return "Choice of card to give"; + } + @Override + public boolean equals(Object obj) { + return obj instanceof ChoiceOfCardToGive cc && cc.giver == giver && cc.recipient == recipient && cc.executed == executed; + } + @Override + public int hashCode() { + return giver * 31 + recipient * 31 * 31 + (executed ? 1 : 0) + 2901; + } +} diff --git a/src/main/java/games/explodingkittens/actions/DefuseKitten.java b/src/main/java/games/explodingkittens/actions/DefuseKitten.java new file mode 100644 index 000000000..a03d3ece5 --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/DefuseKitten.java @@ -0,0 +1,65 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import core.interfaces.IExtendedSequence; +import games.explodingkittens.ExplodingKittensGameState; + +import java.util.List; +import java.util.stream.IntStream; + +import static java.util.stream.Collectors.toList; + +public class DefuseKitten implements IExtendedSequence { + + boolean executed; + final int player; + + + public DefuseKitten(int player) { + this.player = player; + } + + @Override + public List _computeAvailableActions(AbstractGameState state) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) state; + return IntStream.rangeClosed(0, ekgs.getDrawPile().getSize()).mapToObj(PlaceKitten::new).collect(toList()); + } + + @Override + public int getCurrentPlayer(AbstractGameState state) { + return player; + } + + @Override + public void _afterAction(AbstractGameState state, AbstractAction action) { + if (action instanceof PlaceKitten) + executed = true; + } + + @Override + public boolean executionComplete(AbstractGameState state) { + return executed; + } + + @Override + public DefuseKitten copy() { + DefuseKitten retValue = new DefuseKitten(player); + retValue.executed = executed; + return retValue; + } + + @Override + public String toString() { + return "Defuse Kitten"; + } + @Override + public boolean equals(Object obj) { + return obj instanceof DefuseKitten dk && dk.player == player && dk.executed == executed; + } + + @Override + public int hashCode() { + return 9981 - player * 63 + (executed ? 1 : 0); + } +} diff --git a/src/main/java/games/explodingkittens/actions/GiveCard.java b/src/main/java/games/explodingkittens/actions/GiveCard.java new file mode 100644 index 000000000..71a8e4635 --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/GiveCard.java @@ -0,0 +1,66 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import games.explodingkittens.*; +import games.explodingkittens.cards.ExplodingKittensCard; + +import java.util.Objects; + +public class GiveCard extends AbstractAction { + + public int giver; + public int recipient; + public ExplodingKittensCard.CardType cardType; + + public GiveCard(int giver, int recipient, ExplodingKittensCard.CardType cardType) { + this.giver = giver; + this.cardType = cardType; + this.recipient = recipient; + } + + @Override + public boolean execute(AbstractGameState gs) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + ExplodingKittensCard card = state.getPlayerHand(giver).stream() + .filter(c -> c.cardType == cardType).findFirst() + .orElseThrow(() -> new AssertionError("Card not found : " + cardType)); + state.getPlayerHand(giver).remove(card); + state.getPlayerHand(recipient).add(card); + return true; + } + + @Override + public GiveCard copy() { + return this; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof GiveCard gc) { + return gc.giver == giver && gc.recipient == recipient && gc.cardType == cardType; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(giver, recipient, cardType.ordinal()); + } + + @Override + public String toString() { + return "Give card " + cardType + " from player " + giver + " to player " + recipient; + } + + @Override + public String getString(AbstractGameState gameState, int perspectiveID) { + if (perspectiveID == giver || perspectiveID == recipient) return toString(); + return getString(gameState); + } + + @Override + public String getString(AbstractGameState gameState) { + return "Player " + giver + " gives a card to player " + recipient; + } +} diff --git a/src/main/java/games/explodingkittens/actions/Nope.java b/src/main/java/games/explodingkittens/actions/Nope.java new file mode 100644 index 000000000..8eefd2b7f --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/Nope.java @@ -0,0 +1,41 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import games.explodingkittens.ExplodingKittensGameState; +import games.explodingkittens.cards.ExplodingKittensCard; + +public class Nope extends AbstractAction { + + @Override + public boolean execute(AbstractGameState gs) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + state.setInPlay(ExplodingKittensCard.CardType.NOPE, state.getCurrentPlayer()); + return true; + } + + @Override + public AbstractAction copy() { + return this; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof Nope; + } + + @Override + public int hashCode() { + return -42943; + } + + @Override + public String toString() { + return "Nope"; + } + + @Override + public String getString(AbstractGameState gameState) { + return toString(); + } +} diff --git a/src/main/java/games/explodingkittens/actions/NopeableAction.java b/src/main/java/games/explodingkittens/actions/NopeableAction.java new file mode 100644 index 000000000..0b3540769 --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/NopeableAction.java @@ -0,0 +1,119 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import core.components.Deck; +import core.interfaces.IExtendedSequence; +import games.explodingkittens.ExplodingKittensGameState; +import games.explodingkittens.cards.ExplodingKittensCard; + +import java.util.List; +import java.util.Objects; + +import static games.explodingkittens.cards.ExplodingKittensCard.CardType.NOPE; + + +public class NopeableAction implements IExtendedSequence { + + int currentInterrupter = -1; + int lastCardPlayedBy; + PlayEKCard originalAction; + int nopes = 0; + + public NopeableAction(int player, PlayEKCard action, ExplodingKittensGameState state) { + this.lastCardPlayedBy = player; + this.originalAction = action.copy(); + setNextInterrupter(state); + } + + + // private constructor for copying + private NopeableAction(int lastCardPlayedBy, PlayEKCard originalAction, int currentInterrupter, int nopes) { + this.lastCardPlayedBy = lastCardPlayedBy; + this.originalAction = originalAction.copy(); + this.currentInterrupter = currentInterrupter; + this.nopes = nopes; + } + + @Override + public List _computeAvailableActions(AbstractGameState gs) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + Deck hand = state.getPlayerHand(currentInterrupter); + if (hand.stream().anyMatch(c -> c.cardType == NOPE)) { + return List.of(new Pass(), new Nope()); + } + throw new AssertionError(currentInterrupter + " does not have a NOPE card to play"); + } + + + private void setNextInterrupter(ExplodingKittensGameState state) { + if (currentInterrupter == -1) { // initialisation + currentInterrupter = (lastCardPlayedBy + 1) % state.getNPlayers(); + } else if (currentInterrupter != lastCardPlayedBy) { + currentInterrupter = (currentInterrupter + 1) % state.getNPlayers(); + } + // Now we see if anyone has a Nope card + while (currentInterrupter != lastCardPlayedBy) { + if (state.isNotTerminalForPlayer(currentInterrupter)) { + Deck hand = state.getPlayerHand(currentInterrupter); + boolean hasNope = hand.stream().anyMatch(c -> c.cardType == NOPE); + if (hasNope) break; + } + currentInterrupter = (currentInterrupter + 1) % state.getNPlayers(); + } + } + + @Override + public int getCurrentPlayer(AbstractGameState state) { + return currentInterrupter; + } + + @Override + public void _afterAction(AbstractGameState gs, AbstractAction action) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + if (action instanceof Nope) { + nopes++; + // this resets the count + lastCardPlayedBy = currentInterrupter; + currentInterrupter = -1; + setNextInterrupter(state); + } + if (action instanceof Pass) { + setNextInterrupter(state); + } + if (executionComplete(state)) { + // we have gone round the table + if (nopes % 2 == 0) { + // no one noped the action; execute the action + originalAction.cardType.execute(state, originalAction.target); + } + state.getInPlay().forEach(c -> state.getDiscardPile().add(c)); + state.getInPlay().clear(); + } + } + + @Override + public boolean executionComplete(AbstractGameState state) { + return currentInterrupter == lastCardPlayedBy; // we have gone round the table + } + + @Override + public IExtendedSequence copy() { + return new NopeableAction(lastCardPlayedBy, originalAction, currentInterrupter, nopes); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof NopeableAction other)) return false; + return lastCardPlayedBy == other.lastCardPlayedBy && + currentInterrupter == other.currentInterrupter && + nopes == other.nopes && + originalAction.equals(other.originalAction); + } + + @Override + public int hashCode() { + return Objects.hash(lastCardPlayedBy, currentInterrupter, nopes, originalAction); + } +} diff --git a/src/main/java/games/explodingkittens/actions/Pass.java b/src/main/java/games/explodingkittens/actions/Pass.java new file mode 100644 index 000000000..a92653920 --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/Pass.java @@ -0,0 +1,15 @@ +package games.explodingkittens.actions; + +import core.actions.DoNothing; + +public class Pass extends DoNothing { + @Override + public String toString() { + return "Player passes"; + } + + @Override + public Pass copy() { + return this; + } +} diff --git a/src/main/java/games/explodingkittens/actions/PlaceKitten.java b/src/main/java/games/explodingkittens/actions/PlaceKitten.java new file mode 100644 index 000000000..573fa97eb --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/PlaceKitten.java @@ -0,0 +1,60 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import games.explodingkittens.ExplodingKittensGameState; +import games.explodingkittens.cards.ExplodingKittensCard; + +public class PlaceKitten extends AbstractAction { + + public final int index; + + public PlaceKitten(int atIndex) { + this.index = atIndex; + } + + @Override + public boolean execute(AbstractGameState gs) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + int player = state.getCurrentPlayer(); + ExplodingKittensCard card = state.getPlayerHand(player).stream() + .filter(c -> c.cardType == ExplodingKittensCard.CardType.EXPLODING_KITTEN).findFirst() + .orElseThrow(() -> new AssertionError("No Exploding Kitten found")); + state.getPlayerHand(player).remove(card); + boolean[] visibility = new boolean[state.getNPlayers()]; + visibility[state.getCurrentPlayer()] = true; + state.getDrawPile().add(card, index, visibility); + state.setSkip(true); + return true; + } + + @Override + public AbstractAction copy() { + return this; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof PlaceKitten pk && pk.index == index; + } + + @Override + public int hashCode() { + return 3805093 + index; + } + + @Override + public String getString(AbstractGameState gameState, int perspectivePlayer) { + return perspectivePlayer == gameState.getCurrentPlayer() ? toString() : getString(gameState); + } + + @Override + public String getString(AbstractGameState gameState) { + return "Places Exploding Kitten in deck somewhere"; + } + + @Override + public String toString() { + return "Place Exploding Kitten at index " + index; + } +} diff --git a/src/main/java/games/explodingkittens/actions/PlayEKCard.java b/src/main/java/games/explodingkittens/actions/PlayEKCard.java new file mode 100644 index 000000000..323fa08be --- /dev/null +++ b/src/main/java/games/explodingkittens/actions/PlayEKCard.java @@ -0,0 +1,68 @@ +package games.explodingkittens.actions; + +import core.AbstractGameState; +import core.actions.AbstractAction; +import games.explodingkittens.ExplodingKittensGameState; +import games.explodingkittens.cards.ExplodingKittensCard.CardType; + +import java.util.Objects; + +public class PlayEKCard extends AbstractAction { + + public final CardType cardType; + public final int target; + + public PlayEKCard(CardType cardType) { + this.cardType = cardType; + target = -1; + } + + public PlayEKCard(CardType cardType, int target) { + this.cardType = cardType; + this.target = target; + } + + @Override + public boolean execute(AbstractGameState gs) { + ExplodingKittensGameState state = (ExplodingKittensGameState) gs; + state.setInPlay(cardType, state.getCurrentPlayer()); + if (cardType.catCard) + state.setInPlay(cardType, state.getCurrentPlayer()); // add an extra one + + if (cardType.nopeable) { + state.setActionInProgress(new NopeableAction(state.getCurrentPlayer(), this, state)); + } else { + // if not Nopeable, execute the card + cardType.execute(state, target); + } + + return true; + } + + @Override + public PlayEKCard copy() { + return this; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof PlayEKCard pek && pek.cardType == cardType && pek.target == target; + } + + @Override + public int hashCode() { + return Objects.hash(cardType, target) + 2901; + } + + @Override + public String toString() { + if (target == -1) + return "Play " + cardType; + return "Play " + cardType + " on player " + target; + } + + @Override + public String getString(AbstractGameState gameState) { + return toString(); + } +} diff --git a/src/main/java/games/explodingkittens/cards/ExplodingKittensCard.java b/src/main/java/games/explodingkittens/cards/ExplodingKittensCard.java index 943fa0e29..05724394f 100644 --- a/src/main/java/games/explodingkittens/cards/ExplodingKittensCard.java +++ b/src/main/java/games/explodingkittens/cards/ExplodingKittensCard.java @@ -1,22 +1,89 @@ package games.explodingkittens.cards; import core.components.Card; +import games.explodingkittens.ExplodingKittensGameState; +import games.explodingkittens.actions.ChoiceOfCardToGive; +import org.apache.spark.sql.catalyst.plans.logical.BinaryCommand; + +import java.util.function.BiConsumer; + public class ExplodingKittensCard extends Card { + + public static int SEE_THE_FUTURE_CARDS = 3; + public static int ADDITIONAL_TURNS = 2; + public enum CardType { - EXPLODING_KITTEN, - DEFUSE, - NOPE, - ATTACK, - SKIP, - FAVOR, - SHUFFLE, - SEETHEFUTURE, - TACOCAT, - MELONCAT, - FURRYCAT, - BEARDCAT, - RAINBOWCAT, + EXPLODING_KITTEN(false), + DEFUSE(false), + NOPE(false), + ATTACK(true, (state, target) -> { + int attackLevel = ADDITIONAL_TURNS; + state.setSkip(true); + if (state.getCurrentPlayerTurnsLeft() > 1) { + attackLevel += state.getCurrentPlayerTurnsLeft(); + state.setCurrentPlayerTurnsLeft(1); + } + state.setNextAttackLevel(attackLevel); + }), + SKIP(true, (state, target) -> { + state.setSkip(true); + }), + FAVOR(true, (state, target) -> { + int cards = state.getPlayerHand(target).getSize(); + if (cards > 0) { // edge cases make zero cards possible + state.setActionInProgress(new ChoiceOfCardToGive(target, state.getCurrentPlayer())); + } + }), + SHUFFLE(true, (state, target) -> { + state.getDrawPile().shuffle(state.getRnd()); + }), + SEETHEFUTURE(true, (state, target) -> { + // we set the visibility of the top 3 cards of the draw pile to the current player + int cardsToSee = Math.min(SEE_THE_FUTURE_CARDS, state.getDrawPile().getSize()); + for (int i = 0; i < cardsToSee; i++) { + state.getDrawPile().setVisibilityOfComponent(i, state.getCurrentPlayer(), true); + } + }), + TACOCAT(true, true), + MELONCAT(true, true), + FURRYCAT(true, true), + BEARDCAT(true, true), + RAINBOWCAT(true, true); + + public final boolean nopeable; + public final boolean catCard; + private final BiConsumer lambda; + + CardType(boolean nope) { + this(nope, (state, target) -> { + throw new AssertionError("Card should not be executed directly"); + }); + } + + CardType (boolean nope, boolean catCard) { + this.nopeable = nope; + this.catCard = catCard; + // lambda for all cat cards; need to play a pair to take a random card from the target + this.lambda = (state, target) -> { + if (state.getPlayerHand(target).getSize() > 0) { + int index = state.getRnd().nextInt(state.getPlayerHand(target).getSize()); + ExplodingKittensCard card = state.getPlayerHand(target).get(index); + state.getPlayerHand(target).remove(card); + state.getPlayerHand(state.getCurrentPlayer()).add(card); + } + }; + } + + CardType(boolean nope, BiConsumer lambda) { + this.nopeable = nope; + this.catCard = false; + this.lambda = lambda; + } + + public void execute(ExplodingKittensGameState state, int target) { + lambda.accept(state, target); + } } public CardType cardType; @@ -33,7 +100,7 @@ public ExplodingKittensCard(CardType cardType, int ID) { @Override public Card copy() { - return new ExplodingKittensCard(cardType, componentID); + return this; // immutable } @Override diff --git a/src/main/java/games/explodingkittens/gui/ExplodingKittensDiscardView.java b/src/main/java/games/explodingkittens/gui/ExplodingKittensDiscardView.java index 486e8f861..2388b1f5b 100644 --- a/src/main/java/games/explodingkittens/gui/ExplodingKittensDiscardView.java +++ b/src/main/java/games/explodingkittens/gui/ExplodingKittensDiscardView.java @@ -1,23 +1,19 @@ package games.explodingkittens.gui; -import core.actions.AbstractAction; import core.components.Component; import core.components.Deck; -import games.explodingkittens.actions.*; -import games.explodingkittens.actions.reactions.ChooseSeeTheFutureOrder; +import core.interfaces.IExtendedSequence; import games.explodingkittens.cards.ExplodingKittensCard; import java.awt.*; -import java.util.*; +import java.util.ArrayList; +import java.util.Stack; public class ExplodingKittensDiscardView extends ExplodingKittensDeckView { // Images for Action Stack ArrayList catImagesBkup; - // This view adds action stack as cards in the discard pile for display - Stack actionStack; - /** * Constructor initialising information and adding key/mouse listener for card highlight (left click or ALT + hover * allows showing the highlighted card on top of all others). @@ -25,9 +21,8 @@ public class ExplodingKittensDiscardView extends ExplodingKittensDeckView { * @param visible - true if whole deck visible * @param dataPath - path to assets */ - public ExplodingKittensDiscardView(Deck d, Stack actionStack, boolean visible, String dataPath) { + public ExplodingKittensDiscardView(Deck d, boolean visible, String dataPath) { super(-1, d, visible, dataPath); - this.actionStack = actionStack; catImagesBkup = new ArrayList<>(); catImagesBkup.addAll(catImages); } @@ -40,13 +35,6 @@ public void drawDeck(Graphics2D g) { Component oldComponent = component; @SuppressWarnings("unchecked") Deck deckCopy = ((Deck) component).copy(); - // Add cards played from action stack into the copy of the deck - for (AbstractAction aa: actionStack) { - ExplodingKittensCard card = getStackCard(aa); - if (card != null) { - deckCopy.add(card); - } - } // set thie copy to tbe the component we draw component = deckCopy; // and draw it @@ -55,24 +43,5 @@ public void drawDeck(Graphics2D g) { component = oldComponent; } - /** - * Turns an action into a card - * @param aa - action to turn to card - * @return - Exploding kittens card - */ - private ExplodingKittensCard getStackCard(AbstractAction aa) { - if (aa instanceof AttackAction) { - return new ExplodingKittensCard(ExplodingKittensCard.CardType.ATTACK, 0); - } else if (aa instanceof ChooseSeeTheFutureOrder) { - return new ExplodingKittensCard(ExplodingKittensCard.CardType.FAVOR, 1); - } else if (aa instanceof NopeAction) { - return new ExplodingKittensCard(ExplodingKittensCard.CardType.SEETHEFUTURE, 3); - } else if (aa instanceof ShuffleAction) { - return new ExplodingKittensCard(ExplodingKittensCard.CardType.SHUFFLE, 4); - } else if (aa instanceof SkipAction) { - return new ExplodingKittensCard(ExplodingKittensCard.CardType.SKIP, 5); - } - return null; - } } diff --git a/src/main/java/games/explodingkittens/gui/ExplodingKittensGUIManager.java b/src/main/java/games/explodingkittens/gui/ExplodingKittensGUIManager.java index ad8add1f6..07877430b 100644 --- a/src/main/java/games/explodingkittens/gui/ExplodingKittensGUIManager.java +++ b/src/main/java/games/explodingkittens/gui/ExplodingKittensGUIManager.java @@ -1,14 +1,13 @@ package games.explodingkittens.gui; -import gui.AbstractGUIManager; -import gui.GamePanel; import core.AbstractGameState; import core.AbstractPlayer; import core.Game; -import games.explodingkittens.ExplodingKittensParameters; -import games.explodingkittens.ExplodingKittensGameState; +import gui.AbstractGUIManager; +import gui.GamePanel; import gui.IScreenHighlight; import players.human.ActionController; +import games.explodingkittens.*; import javax.swing.*; import javax.swing.border.Border; @@ -29,6 +28,7 @@ public class ExplodingKittensGUIManager extends AbstractGUIManager { ExplodingKittensDeckView[] playerHands; // Discard pile view ExplodingKittensDiscardView discardPile; + ExplodingKittensDiscardView inPlayPile; // Draw pile view ExplodingKittensDeckView drawPile; @@ -68,7 +68,7 @@ public ExplodingKittensGUIManager(GamePanel parent, Game game, ActionController JPanel[] sides = new JPanel[]{new JPanel(), new JPanel(), new JPanel(), new JPanel()}; int next = 0; for (int i = 0; i < nPlayers; i++) { - ExplodingKittensDeckView playerHand = new ExplodingKittensDeckView(i, ekgs.getPlayerHandCards().get(i), false, ekgp.getDataPath()); + ExplodingKittensDeckView playerHand = new ExplodingKittensDeckView(humanPlayerIds.iterator().next(), ekgs.getPlayerHand(i), false, ekgp.getDataPath()); // Get agent name String[] split = game.getPlayers().get(i).getClass().toString().split("\\."); @@ -92,10 +92,12 @@ public ExplodingKittensGUIManager(GamePanel parent, Game game, ActionController // Discard and draw piles go in the center JPanel centerArea = new JPanel(); centerArea.setLayout(new BoxLayout(centerArea, BoxLayout.Y_AXIS)); - discardPile = new ExplodingKittensDiscardView(ekgs.getDiscardPile(), ekgs.getActionStack(), true, ekgp.getDataPath()); - drawPile = new ExplodingKittensDeckView(-1, ekgs.getDrawPile(), gameState.getCoreGameParameters().alwaysDisplayFullObservable, ekgp.getDataPath()); + discardPile = new ExplodingKittensDiscardView(ekgs.getDiscardPile(), true, ekgp.getDataPath()); + inPlayPile = new ExplodingKittensDiscardView(ekgs.getInPlay(), true, ekgp.getDataPath()); + drawPile = new ExplodingKittensDeckView(humanPlayerIds.iterator().next(), ekgs.getDrawPile(), gameState.getCoreGameParameters().alwaysDisplayFullObservable, ekgp.getDataPath()); centerArea.add(drawPile); centerArea.add(discardPile); + centerArea.add(inPlayPile); JPanel jp = new JPanel(); jp.setLayout(new GridBagLayout()); jp.add(centerArea); @@ -136,9 +138,9 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { // Update decks and visibility ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; for (int i = 0; i < gameState.getNPlayers(); i++) { - playerHands[i].updateComponent(ekgs.getPlayerHandCards().get(i)); + playerHands[i].updateComponent(ekgs.getPlayerHand(i).copy()); if (i == gameState.getCurrentPlayer() && gameState.getCoreGameParameters().alwaysDisplayCurrentPlayer - || humanPlayerId.contains(i) + || humanPlayerIds.contains(i) || gameState.getCoreGameParameters().alwaysDisplayFullObservable) { playerHands[i].setFront(true); playerHands[i].setFocusable(true); @@ -155,11 +157,9 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { playerHands[i].setBorder(playerViewBorders[i]); } } - discardPile.updateComponent(ekgs.getDiscardPile()); + discardPile.updateComponent(ekgs.getDiscardPile().copy()); discardPile.setFocusable(true); - drawPile.updateComponent(ekgs.getDrawPile()); - if (humanPlayerId.contains(activePlayer) || gameState.getCoreGameParameters().alwaysDisplayFullObservable) - drawPile.setFront(true); + drawPile.updateComponent(ekgs.getDrawPile().copy()); } } diff --git a/src/main/java/games/explodingkittens/ExplodingKittensFeatures.java b/src/main/java/games/explodingkittensOLD/ExplodingKittensFeatures.java similarity index 96% rename from src/main/java/games/explodingkittens/ExplodingKittensFeatures.java rename to src/main/java/games/explodingkittensOLD/ExplodingKittensFeatures.java index 31526060b..ed6e3ea73 100644 --- a/src/main/java/games/explodingkittens/ExplodingKittensFeatures.java +++ b/src/main/java/games/explodingkittensOLD/ExplodingKittensFeatures.java @@ -1,9 +1,9 @@ -package games.explodingkittens; +package games.explodingkittensOLD; import core.AbstractGameState; import core.CoreConstants; import core.interfaces.IStateFeatureVector; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/games/explodingkittensOLD/ExplodingKittensForwardModel.java b/src/main/java/games/explodingkittensOLD/ExplodingKittensForwardModel.java new file mode 100644 index 000000000..d83de9c64 --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/ExplodingKittensForwardModel.java @@ -0,0 +1,496 @@ +package games.explodingkittensOLD; + +import core.AbstractForwardModel; +import core.AbstractGameState; +import core.CoreConstants; +import core.CoreConstants.VisibilityMode; +import core.actions.AbstractAction; +import core.actions.ActionSpace; +import core.components.Deck; +import core.components.PartialObservableDeck; +import core.interfaces.ITreeActionSpace; +import games.GameType; +import games.explodingkittensOLD.actions.*; +import games.explodingkittensOLD.actions.reactions.ChooseSeeTheFutureOrder; +import games.explodingkittensOLD.actions.reactions.GiveCard; +import games.explodingkittensOLD.actions.reactions.PassAction; +import games.explodingkittensOLD.actions.reactions.PlaceExplodingKitten; +import games.explodingkittensOLD.cards.ExplodingKittensCard; +import utilities.ActionTreeNode; + +import java.util.*; + +import static games.explodingkittensOLD.ExplodingKittensGameState.ExplodingKittensGamePhase.Nope; +import static utilities.Utils.generatePermutations; + +public class ExplodingKittensForwardModel extends AbstractForwardModel implements ITreeActionSpace { + private final ArrayList cardTypes = new ArrayList<>(Arrays.asList("EXPLODING_KITTEN", "DEFUSE", "NOPE", "ATTACK", "SKIP", "FAVOR", + "SHUFFLE", "SEETHEFUTURE", "TACOCAT", "MELONCAT", "FURRYCAT", "BEARDCAT", "RAINBOWCAT")); + private final int N_CARDS_TO_CHECK = 10; + + /** + * Performs initial game setup according to game rules. + * @param firstState - the state to be modified to the initial game state. + */ + protected void _setup(AbstractGameState firstState) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState)firstState; + ExplodingKittensParameters ekp = (ExplodingKittensParameters)firstState.getGameParameters(); + ekgs.playerHandCards = new ArrayList<>(); + ekgs.playerGettingAFavor = -1; + ekgs.actionStack = null; + // Set up draw pile deck + PartialObservableDeck drawPile = new PartialObservableDeck<>("Draw Pile", -1, firstState.getNPlayers(), VisibilityMode.HIDDEN_TO_ALL); + ekgs.setDrawPile(drawPile); + + // Add all cards but defuse and exploding kittens + for (HashMap.Entry entry : ekp.cardCounts.entrySet()) { + if (entry.getKey() == ExplodingKittensCard.CardType.DEFUSE || entry.getKey() == ExplodingKittensCard.CardType.EXPLODING_KITTEN) + continue; + for (int i = 0; i < entry.getValue(); i++) { + ExplodingKittensCard card = new ExplodingKittensCard(entry.getKey()); + drawPile.add(card); + } + } + ekgs.getDrawPile().shuffle(ekgs.getRnd()); + + // Set up player hands + List> playerHandCards = new ArrayList<>(firstState.getNPlayers()); + for (int i = 0; i < firstState.getNPlayers(); i++) { + boolean[] visible = new boolean[firstState.getNPlayers()]; + visible[i] = true; + PartialObservableDeck playerCards = new PartialObservableDeck<>("Player Cards", i, visible); + playerHandCards.add(playerCards); + + // Add defuse card + ExplodingKittensCard defuse = new ExplodingKittensCard(ExplodingKittensCard.CardType.DEFUSE); + defuse.setOwnerId(i); + playerCards.add(defuse); + + // Add N random cards from the deck + for (int j = 0; j < ekp.nCardsPerPlayer; j++) { + ExplodingKittensCard c = ekgs.getDrawPile().draw(); + c.setOwnerId(i); + playerCards.add(c); + } + } + ekgs.setPlayerHandCards(playerHandCards); + ekgs.setDiscardPile(new Deck<>("Discard Pile", VisibilityMode.VISIBLE_TO_ALL)); + + // Add remaining defuse cards and exploding kitten cards to the deck and shuffle again + for (int i = ekgs.getNPlayers(); i < ekp.nDefuseCards; i++){ + ExplodingKittensCard defuse = new ExplodingKittensCard(ExplodingKittensCard.CardType.DEFUSE); + drawPile.add(defuse); + } + for (int i = 0; i < ekgs.getNPlayers() + ekp.cardCounts.get(ExplodingKittensCard.CardType.EXPLODING_KITTEN); i++){ + ExplodingKittensCard explodingKitten = new ExplodingKittensCard(ExplodingKittensCard.CardType.EXPLODING_KITTEN); + drawPile.add(explodingKitten); + } + drawPile.shuffle(ekgs.getRnd()); + + ekgs.setActionStack(new Stack<>()); + ekgs.orderOfPlayerDeath = new int[ekgs.getNPlayers()]; + ekgs.setGamePhase(CoreConstants.DefaultGamePhase.Main); + } + + /** + * Applies the given action to the game state and executes any other game rules. + * @param gameState - current game state, to be modified by the action. + * @param action - action requested to be played by a player. + */ + @Override + protected void _next(AbstractGameState gameState, AbstractAction action) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; + ExplodingKittensTurnOrder ekTurnOrder = (ExplodingKittensTurnOrder) ekgs.getTurnOrder(); + Stack actionStack = ekgs.getActionStack(); + + if (action instanceof IsNopeable) { + actionStack.add(action); + ((IsNopeable) action).actionPlayed(ekgs); + + ekTurnOrder.registerNopeableActionByPlayer(ekgs, ekTurnOrder.getCurrentPlayer(ekgs)); + if (action instanceof NopeAction) { + // Nope cards added immediately to avoid infinite nopeage + action.execute(ekgs); + } else { + if (ekTurnOrder.reactionsFinished()){ + action.execute(ekgs); + actionStack.clear(); + } + } + } else if (action instanceof PassAction) { + + ekTurnOrder.endPlayerTurnStep(gameState); + + if (ekTurnOrder.reactionsFinished()) { + // apply stack + if (actionStack.size()%2 == 0){ + while (actionStack.size() > 1) { + actionStack.pop(); + } + //Action was successfully noped + ((IsNopeable) actionStack.pop()).nopedExecute(gameState); +// if (gameState.getCoreGameParameters().verbose) { +// System.out.println("Action was successfully noped"); +// } + } else { +// if (actionStack.size() > 2 && gameState.getCoreGameParameters().verbose) { +// System.out.println("All nopes were noped"); +// } + + while (actionStack.size() > 1) { + actionStack.pop(); + } + + //Action can be played + AbstractAction stackedAction = actionStack.get(0); + stackedAction.execute(gameState); + } + actionStack.clear(); + if (ekgs.getGamePhase() == Nope) { + ekgs.setGamePhase(CoreConstants.DefaultGamePhase.Main); + } + } + } else { + action.execute(gameState); + } + } + + @Override + protected List _computeAvailableActions(AbstractGameState gameState) { + return _computeAvailableActions(gameState, ActionSpace.Default); + } + + /** + * Calculates the list of currently available actions, possibly depending on the game phase. + * @return - List of AbstractAction objects. + */ + @Override + protected List _computeAvailableActions(AbstractGameState gameState, ActionSpace actionSpace) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; + ArrayList actions; + + // The actions per player do not change a lot in between two turns + // Could update an existing list instead of generating a new list every time we query this function + + // Find actions for the player depending on current game phase + int player = ekgs.getCurrentPlayer(); + if (CoreConstants.DefaultGamePhase.Main.equals(ekgs.getGamePhase())) { + actions = playerActions(ekgs, player); + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Defuse.equals(ekgs.getGamePhase())) { + actions = placeKittenActions(ekgs, player); + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Nope.equals(ekgs.getGamePhase())) { + actions = nopeActions(ekgs, player); + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Favor.equals(ekgs.getGamePhase())) { + actions = favorActions(ekgs, player); + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.SeeTheFuture.equals(ekgs.getGamePhase())) { + actions = seeTheFutureActions(ekgs, player); + } else { + actions = new ArrayList<>(); + } + + return actions; + } + + @Override + protected AbstractForwardModel _copy() { + return new ExplodingKittensForwardModel(); + } + + @Override + protected void endPlayerTurn(AbstractGameState state) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) state; + ekgs.getTurnOrder().endPlayerTurn(ekgs); + } + + private ArrayList playerActions(ExplodingKittensGameState ekgs, int playerID){ + ArrayList actions = new ArrayList<>(); + Deck playerDeck = ekgs.playerHandCards.get(playerID); +// ActionTreeNode playNode = root.findChildrenByName("PLAY"); + + HashSet types = new HashSet<>(); + for (int c = 0; c < playerDeck.getSize(); c++) { + ExplodingKittensCard card = playerDeck.get(c); + if (types.contains(card.cardType)) continue; + types.add(card.cardType); + + switch (card.cardType) { + case DEFUSE: + case MELONCAT: + case RAINBOWCAT: + case FURRYCAT: + case BEARDCAT: + case TACOCAT: + case NOPE: + case EXPLODING_KITTEN: + break; + case SKIP: +// playNode.setValue(1); + actions.add(new SkipAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); +// playNode.findChildrenByName("SKIP").setAction(new SkipAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); + break; + case FAVOR: + for (int player = 0; player < ekgs.getNPlayers(); player++) { + if (player == playerID) + continue; + if (ekgs.playerHandCards.get(player).getSize() > 0) { + actions.add(new FavorAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, player)); +// playNode.setValue(1); +// playNode.findChildrenByName("FAVOR").setAction(new FavorAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, player)); + } + } + break; + case ATTACK: + for (int targetPlayer = 0; targetPlayer < ekgs.getNPlayers(); targetPlayer++) { + + if (targetPlayer == playerID || ekgs.getPlayerResults()[targetPlayer] != CoreConstants.GameResult.GAME_ONGOING) + continue; + + actions.add(new AttackAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, targetPlayer)); +// playNode.setValue(1); +// playNode.findChildrenByName("ATTACK").setAction(new AttackAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, targetPlayer)); + } + break; + case SHUFFLE: + actions.add(new ShuffleAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); +// playNode.setValue(1); +// playNode.findChildrenByName("SHUFFLE").setAction(new ShuffleAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); + break; + case SEETHEFUTURE: +// actions.add(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); +// playNode.setValue(1); +// playNode.findChildrenByName("SEETHEFUTURE").setAction(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); + break; + default: + System.out.println("No actions known for cardtype: " + card.cardType); + } + } + /* todo add special combos + // can take any card from anyone + for (int i = 0; i < nPlayers; i++){ + if (i != activePlayer){ + Deck otherDeck = (Deck)this.areas.get(activePlayer).getComponent(playerHandHash); + for (Card card: otherDeck.getCards()){ + core.actions.add(new TakeCard(card, i)); + } + } + }*/ + + // add end turn by drawing a card + actions.add(new DrawExplodingKittenCard(ekgs.drawPile.getComponentID(), playerDeck.getComponentID())); +// root.findChildrenByName("DRAW").setAction(new DrawExplodingKittenCard(ekgs.drawPile.getComponentID(), playerDeck.getComponentID())); + return actions; + } + + private ArrayList placeKittenActions(ExplodingKittensGameState ekgs, int playerID){ + ArrayList actions = new ArrayList<>(); + Deck playerDeck = ekgs.playerHandCards.get(playerID); + int explodingKittenCard = -1; + for (int i = 0; i < playerDeck.getSize(); i++) { + if (playerDeck.getComponents().get(i).cardType == ExplodingKittensCard.CardType.EXPLODING_KITTEN) { + explodingKittenCard = i; + break; + } + } + if (explodingKittenCard != -1) { + for (int i = 0; i <= ekgs.drawPile.getSize(); i++) { + actions.add(new PlaceExplodingKitten(playerDeck.getComponentID(), ekgs.drawPile.getComponentID(), explodingKittenCard, i)); +// if (i < N_CARDS_TO_CHECK){ +// root.findChildrenByName("DEFUSE").findChildrenByName("PUT" + i, true).setAction(new PlaceExplodingKitten(playerDeck.getComponentID(), ekgs.drawPile.getComponentID(), explodingKittenCard, i)); +// } + } + } + return actions; + } + + private ArrayList nopeActions(ExplodingKittensGameState ekgs, int playerID){ + ArrayList actions = new ArrayList<>(); + Deck playerDeck = ekgs.playerHandCards.get(playerID); + for (int c = 0; c < playerDeck.getSize(); c++) { + if (playerDeck.getComponents().get(c).cardType == ExplodingKittensCard.CardType.NOPE) { + actions.add(new NopeAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); +// root.findChildrenByName("NOPE").findChildrenByName("NOPE").setAction(new NopeAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); + break; + } + } +// root.findChildrenByName("NOPE").findChildrenByName("PASS").setAction(new PassAction()); + actions.add(new PassAction()); + return actions; + } + + private ArrayList favorActions(ExplodingKittensGameState ekgs, int playerID){ + ArrayList actions = new ArrayList<>(); + Deck playerDeck = ekgs.playerHandCards.get(playerID); + Deck receiverDeck = ekgs.playerHandCards.get(ekgs.playerGettingAFavor); + // todo this requires mapping the card type to indices + for (int card = 0; card < playerDeck.getSize(); card++) { + actions.add(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), card)); +// root.findChildrenByName("FAVOR").findChildrenByName(playerDeck.get(card).toString()).setAction(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), card)); + } + if (actions.isEmpty()) // the target has no cards. + actions.add(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), -1)); + return actions; + } + + private ArrayList seeTheFutureActions(ExplodingKittensGameState ekgs, int playerID){ + ArrayList actions = new ArrayList<>(); + Deck playerDeck = ekgs.playerHandCards.get(playerID); + + int cardIdx = -1; + for (int c = 0; c < playerDeck.getSize(); c++) { + if (playerDeck.get(c).cardType == ExplodingKittensCard.CardType.SEETHEFUTURE) { + cardIdx = c; + break; + } + } + + if (cardIdx != -1) { + int numberOfCards = ekgs.drawPile.getSize(); + int n = Math.min(((ExplodingKittensParameters) ekgs.getGameParameters()).nSeeFutureCards, numberOfCards); + if (n > 0) { + + ArrayList permutations = new ArrayList<>(); + int[] order = new int[n]; + for (int i = 0; i < n; i++) { + order[i] = i; + } + generatePermutations(n, order, permutations); + for (int[] perm : permutations) { + actions.add(new ChooseSeeTheFutureOrder(playerDeck.getComponentID(), + ekgs.discardPile.getComponentID(), cardIdx, ekgs.drawPile.getComponentID(), perm)); + } + } + } else { + throw new AssertionError("Player " + playerID + " does not have a SeeTheFuture card in hand."); + } + + return actions; + } + + @Override + public ActionTreeNode updateActionTree(ActionTreeNode root, AbstractGameState gameState) { + root.resetTree(); + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; + + int playerID = ekgs.getCurrentPlayer(); + Deck playerDeck = ekgs.playerHandCards.get(playerID); + if (CoreConstants.DefaultGamePhase.Main.equals(ekgs.getGamePhase())) { + + ActionTreeNode playNode = root.findChildrenByName("PLAY"); + + HashSet types = new HashSet<>(); + for (int c = 0; c < playerDeck.getSize(); c++) { + ExplodingKittensCard card = playerDeck.get(c); + if (types.contains(card.cardType)) continue; + types.add(card.cardType); + + switch (card.cardType) { + case SKIP: + playNode.setValue(1); + playNode.findChildrenByName("SKIP").setAction(new SkipAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); + break; + case FAVOR: + for (int player = 0; player < ekgs.getNPlayers(); player++) { + if (player == playerID) + continue; + if (ekgs.playerHandCards.get(player).getSize() > 0) { + playNode.setValue(1); + playNode.findChildrenByName("FAVOR").setAction(new FavorAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, player)); + } + } + break; + case ATTACK: + for (int targetPlayer = 0; targetPlayer < ekgs.getNPlayers(); targetPlayer++) { + + if (targetPlayer == playerID || ekgs.getPlayerResults()[targetPlayer] != CoreConstants.GameResult.GAME_ONGOING) + continue; + + playNode.setValue(1); + playNode.findChildrenByName("ATTACK").setAction(new AttackAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, targetPlayer)); + } + break; + case SHUFFLE: + playNode.setValue(1); + playNode.findChildrenByName("SHUFFLE").setAction(new ShuffleAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); + break; + case SEETHEFUTURE: + // todo this action not implemented correctly + // actions.add(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); +// playNode.setValue(1); + // playNode.findChildrenByName("SEETHEFUTURE").setAction(new SeeTheFuture(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c, playerID)); + break; +// default: +// System.out.println("No actions known for cardtype: " + card.cardType); + } + } + + // add end turn by drawing a card + root.findChildrenByName("DRAW").setAction(new DrawExplodingKittenCard(ekgs.drawPile.getComponentID(), playerDeck.getComponentID())); + + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Defuse.equals(ekgs.getGamePhase())) { + int explodingKittenCard = -1; + for (int i = 0; i < playerDeck.getSize(); i++) { + if (playerDeck.getComponents().get(i).cardType == ExplodingKittensCard.CardType.EXPLODING_KITTEN) { + explodingKittenCard = i; + break; + } + } + if (explodingKittenCard != -1) { + for (int i = 0; i <= ekgs.drawPile.getSize(); i++) { + if (i < N_CARDS_TO_CHECK){ + root.findChildrenByName("DEFUSE").findChildrenByName("PUT" + i, true).setAction(new PlaceExplodingKitten(playerDeck.getComponentID(), ekgs.drawPile.getComponentID(), explodingKittenCard, i)); + } + } + } + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Nope.equals(ekgs.getGamePhase())) { + for (int c = 0; c < playerDeck.getSize(); c++) { + if (playerDeck.getComponents().get(c).cardType == ExplodingKittensCard.CardType.NOPE) { + root.findChildrenByName("NOPE").findChildrenByName("NOPE").setAction(new NopeAction(playerDeck.getComponentID(), ekgs.discardPile.getComponentID(), c)); + break; + } + } + root.findChildrenByName("NOPE").findChildrenByName("PASS").setAction(new PassAction()); + System.out.println("got boht nope actions"); + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.Favor.equals(ekgs.getGamePhase())) { + Deck receiverDeck = ekgs.playerHandCards.get(ekgs.playerGettingAFavor); + // this requires mapping the card type to indices + for (int card = 0; card < playerDeck.getSize(); card++) { + root.findChildrenByName("FAVOR").findChildrenByName(playerDeck.get(card).toString()).setAction(new GiveCard(playerDeck.getComponentID(), receiverDeck.getComponentID(), card)); + } + } else if (ExplodingKittensGameState.ExplodingKittensGamePhase.SeeTheFuture.equals(ekgs.getGamePhase())) { + // todo this is not correctly implemented + } + + return root; + } + + public ActionTreeNode initActionTree(AbstractGameState gs){ + // set up the action tree + ActionTreeNode tree = new ActionTreeNode(0, "root"); + tree.addChild(0, "DRAW"); + ActionTreeNode play = tree.addChild(0, "PLAY"); + ActionTreeNode favor = tree.addChild(0, "FAVOR"); + ActionTreeNode nope = tree.addChild(0, "NOPE"); + ActionTreeNode defuse = tree.addChild(0, "DEFUSE"); + + for (String cardType : cardTypes) { + ActionTreeNode cardTypeNode = play.addChild(0, cardType); + // can ask a favor from any other player + if (cardType.equals("FAVOR")) { + for (int j = 0; j < GameType.ExplodingKittens.getMaxPlayers(); j++) { + cardTypeNode.addChild(0, "PLAYER" + j); + } + } + favor.addChild(0, cardType); + } + // if player has a nope card it can decide if it wants to play it or not + nope.addChild(0, "NOPE"); + nope.addChild(0, "PASS"); + + // allow to put the back the defuse card to these positions + for (int i =0; i < N_CARDS_TO_CHECK; i++){ + defuse.addChild(0, "PUT"+i); + + } + return tree; + } +} diff --git a/src/main/java/games/explodingkittensOLD/ExplodingKittensGameState.java b/src/main/java/games/explodingkittensOLD/ExplodingKittensGameState.java new file mode 100644 index 000000000..003cfc0eb --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/ExplodingKittensGameState.java @@ -0,0 +1,290 @@ +package games.explodingkittensOLD; + +import core.AbstractGameStateWithTurnOrder; +import core.AbstractParameters; +import core.CoreConstants; +import core.actions.AbstractAction; +import core.components.Component; +import core.components.Deck; +import core.components.PartialObservableDeck; +import core.interfaces.IGamePhase; +import core.interfaces.IPrintable; +import core.turnorders.TurnOrder; +import games.GameType; +import games.explodingkittensOLD.cards.ExplodingKittensCard; + +import java.util.*; + +import static core.CoreConstants.VisibilityMode; + +public class ExplodingKittensGameState extends AbstractGameStateWithTurnOrder implements IPrintable { + + // Cards in each player's hand, index corresponds to player ID + List> playerHandCards; + // Cards in the draw pile + PartialObservableDeck drawPile; + // Cards in the discard pile + Deck discardPile; + // Player ID of the player currently getting a favor + int playerGettingAFavor; + // Current stack of actions + Stack actionStack; + int[] orderOfPlayerDeath; + public ExplodingKittensGameState(AbstractParameters gameParameters, int nPlayers) { + super(gameParameters, nPlayers); + playerGettingAFavor = -1; + playerHandCards = new ArrayList<>(); + } + @Override + protected TurnOrder _createTurnOrder(int nPlayers) { + return new ExplodingKittensTurnOrder(nPlayers); + } + + @Override + protected GameType _getGameType() { + return GameType.ExplodingKittens; + } + + @Override + protected List _getAllComponents() { + return new ArrayList() {{ + add(drawPile); + add(discardPile); + addAll(playerHandCards); + }}; + } + + @Override + protected AbstractGameStateWithTurnOrder __copy(int playerId) { + ExplodingKittensGameState ekgs = new ExplodingKittensGameState(gameParameters.copy(), getNPlayers()); + ekgs.discardPile = discardPile.copy(); + ekgs.playerGettingAFavor = playerGettingAFavor; + ekgs.actionStack = new Stack<>(); + for (AbstractAction a : actionStack) { + ekgs.actionStack.add(a.copy()); + } + ekgs.orderOfPlayerDeath = orderOfPlayerDeath.clone(); + ekgs.playerHandCards = new ArrayList<>(); + for (PartialObservableDeck d : playerHandCards) { + ekgs.playerHandCards.add(d.copy()); + } + ekgs.drawPile = drawPile.copy(); + if (getCoreGameParameters().partialObservable && playerId != -1) { + // Other player hands + draw deck are hidden, combine in draw pile and shuffle + // Note: this considers the agent to track opponent's cards that are known to him by itself + // e.g. in case the agent has previously given a favor card to its opponent + for (int i = 0; i < getNPlayers(); i++) { + if (i != playerId) { + // Take all cards the player can't see from other players and put them in the draw pile. + ArrayList cs = new ArrayList<>(); + for (int j = 0; j < ekgs.playerHandCards.get(i).getSize(); j++) { + if (!ekgs.playerHandCards.get(i).isComponentVisible(j, playerId)) { + ExplodingKittensCard c = ekgs.playerHandCards.get(i).get(j); + ekgs.drawPile.add(c, ekgs.playerHandCards.get(i).getVisibilityOfComponent(j).clone()); + cs.add(c); + } + } + for (ExplodingKittensCard c : cs) { + ekgs.playerHandCards.get(i).remove(c); + } + } + } + + // Shuffles only hidden cards in draw pile, if player knows what's on top those will stay in place + ekgs.drawPile.redeterminiseUnknown(redeterminisationRnd, playerId); + Deck explosive = new Deck<>("tmp", VisibilityMode.HIDDEN_TO_ALL); + for (int i = 0; i < getNPlayers(); i++) { + if (i != playerId) { + for (int j = 0; j < playerHandCards.get(i).getSize(); j++) { + // Add back random cards for all components not visible to this player + if (playerHandCards.get(i).isComponentVisible(j, playerId)) continue; + boolean added = false; + int cardIndex = 0; + while (!added) { + // if the card is visible to the player we cannot move it somewhere else + if (ekgs.drawPile.getVisibilityForPlayer(cardIndex, playerId)) { + cardIndex++; + continue; + } + ExplodingKittensCard card = ekgs.drawPile.pick(cardIndex); + if (card.cardType != ExplodingKittensCard.CardType.EXPLODING_KITTEN) { + ekgs.playerHandCards.get(i).add(card); + added = true; + } else { + explosive.add(card); + } + } + } + } + } + ekgs.drawPile.add(explosive); + } + return ekgs; + } + + @Override + protected double _getHeuristicScore(int playerId) { + return new ExplodingKittensHeuristic().evaluateState(this, playerId); + } + + /** + * This provides the current score in game turns. This will only be relevant for games that have the concept + * of victory points, etc. + * If a game does not support this directly, then just return 0.0 + * + * @param playerId + * @return - double, score of current state + */ + @Override + public double getGameScore(int playerId) { + if (playerResults[playerId] == CoreConstants.GameResult.LOSE_GAME) + // knocked out + return orderOfPlayerDeath[playerId]; + // otherwise our current score is the number knocked out + 1 + return Arrays.stream(playerResults).filter(status -> status == CoreConstants.GameResult.LOSE_GAME).count() + 1; + } + + @Override + public int getOrdinalPosition(int playerId) { + if (playerResults[playerId] == CoreConstants.GameResult.WIN_GAME) + return 1; + if (playerResults[playerId] == CoreConstants.GameResult.LOSE_GAME) + return getNPlayers() - orderOfPlayerDeath[playerId] + 1; + return 1; // anyone still alive is jointly winning + } + + @Override + protected boolean _equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExplodingKittensGameState)) return false; + if (!super.equals(o)) return false; + ExplodingKittensGameState gameState = (ExplodingKittensGameState) o; + return playerGettingAFavor == gameState.playerGettingAFavor && + Objects.equals(playerHandCards, gameState.playerHandCards) && + Objects.equals(drawPile, gameState.drawPile) && + Objects.equals(discardPile, gameState.discardPile) && + Objects.equals(actionStack, gameState.actionStack); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), playerHandCards, drawPile, discardPile, playerGettingAFavor, actionStack); + } + + /** + * Marks a player as dead. + * + * @param playerID - player who was killed in a kitten explosion. + */ + public void killPlayer(int playerID) { + setPlayerResult(CoreConstants.GameResult.LOSE_GAME, playerID); + int nPlayersActive = 0; + for (int i = 0; i < getNPlayers(); i++) { + if (playerResults[i] == CoreConstants.GameResult.GAME_ONGOING) nPlayersActive++; + } + orderOfPlayerDeath[playerID] = getNPlayers() - nPlayersActive; + if (nPlayersActive == 1) { + this.gameStatus = CoreConstants.GameResult.GAME_END; + for (int i = 0; i < getNPlayers(); i++) { + // set winner + if (playerResults[i] == CoreConstants.GameResult.GAME_ONGOING) { + playerResults[i] = CoreConstants.GameResult.WIN_GAME; + } + } + turnOrder.endGame(this); + } + + ((ExplodingKittensTurnOrder) getTurnOrder()).endPlayerTurnStep(this); + + } + + // Getters, setters + public int getPlayerGettingAFavor() { + return playerGettingAFavor; + } + + public void setPlayerGettingAFavor(int playerGettingAFavor) { + this.playerGettingAFavor = playerGettingAFavor; + } + + public PartialObservableDeck getDrawPile() { + return drawPile; + } + + protected void setDrawPile(PartialObservableDeck drawPile) { + this.drawPile = drawPile; + } + + public Deck getDiscardPile() { + return discardPile; + } + + // Protected, only accessible in this package and subclasses + protected void setDiscardPile(Deck discardPile) { + this.discardPile = discardPile; + } + + public Stack getActionStack() { + return actionStack; + } + + protected void setActionStack(Stack actionStack) { + this.actionStack = actionStack; + } + + public List> getPlayerHandCards() { + return playerHandCards; + } + + protected void setPlayerHandCards(List> playerHandCards) { + this.playerHandCards = playerHandCards; + } + + public void printToConsole() { + System.out.println(toString()); + } + + + // Printing functions for the game state and decks. + + @Override + public String toString() { + String s = "============================\n"; + + int currentPlayer = turnOrder.getCurrentPlayer(this); + + for (int i = 0; i < getNPlayers(); i++) { + if (currentPlayer == i) + s += ">>> Player " + i + ":"; + else + s += "Player " + i + ":"; + s += playerHandCards.get(i).toString() + "\n"; + } + + s += "\nDrawPile: "; + s += drawPile.toString() + "\n"; + + s += "DiscardPile: "; + s += discardPile.toString() + "\n"; + + s += "Action stack: "; + for (AbstractAction a : actionStack) { + s += a.toString() + ","; + } + s = s.substring(0, s.length() - 1); + s += "\n\n"; + + s += "Current GamePhase: " + gamePhase + "\n"; + s += "Missing Draws: " + ((ExplodingKittensTurnOrder) turnOrder).requiredDraws + "\n"; + s += "============================\n"; + return s; + } + + // Exploding kittens adds 4 phases on top of default ones. + public enum ExplodingKittensGamePhase implements IGamePhase { + Nope, + Defuse, + Favor, + SeeTheFuture + } +} diff --git a/src/main/java/games/explodingkittensOLD/ExplodingKittensHeuristic.java b/src/main/java/games/explodingkittensOLD/ExplodingKittensHeuristic.java new file mode 100644 index 000000000..09043dc60 --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/ExplodingKittensHeuristic.java @@ -0,0 +1,111 @@ +package games.explodingkittensOLD; + +import core.AbstractGameState; +import core.CoreConstants; +import core.interfaces.IStateHeuristic; +import evaluation.optimisation.TunableParameters; +import games.explodingkittensOLD.cards.ExplodingKittensCard; + +public class ExplodingKittensHeuristic extends TunableParameters implements IStateHeuristic { + + double explodingValue = -1; + double defuseValue = 1; + double regularValue = -0.01; + double seeFutureValue = -0.3; // Play it + double nopeValue = -0.5; // Play it + double attackValue = -0.4; // Play it + double skipValue = 0.2; + double favorValue = -0.1; + double shuffleValue = -0.2; + + public ExplodingKittensHeuristic() { + addTunableParameter("explodingValue", -1.0); + addTunableParameter("defuseValue", 1.0); + addTunableParameter("regularValue", -0.01); + addTunableParameter("seeFutureValue", -0.3); + addTunableParameter("nopeValue", -0.5); + addTunableParameter("attackValue", -0.4); + addTunableParameter("skipValue", 0.2); + addTunableParameter("favorValue", -0.1); + addTunableParameter("shuffleValue", -0.2); + } + + @Override + public void _reset() { + explodingValue = (double) getParameterValue("explodingValue"); + defuseValue = (double) getParameterValue("defuseValue"); + regularValue = (double) getParameterValue("regularValue"); + seeFutureValue = (double) getParameterValue("seeFutureValue"); + nopeValue = (double) getParameterValue("nopeValue"); + attackValue = (double) getParameterValue("attackValue"); + skipValue = (double) getParameterValue("skipValue"); + favorValue = (double) getParameterValue("favorValue"); + shuffleValue = (double) getParameterValue("shuffleValue"); + } + + @Override + public double evaluateState(AbstractGameState gs, int playerId) { + ExplodingKittensGameState ekgs = (ExplodingKittensGameState)gs; + CoreConstants.GameResult playerResult = ekgs.getPlayerResults()[playerId]; + + if (playerResult == CoreConstants.GameResult.LOSE_GAME) + return -1; + if (playerResult == CoreConstants.GameResult.WIN_GAME) + return 1; + + double cardValues = 0.0; + for (ExplodingKittensCard card : ekgs.playerHandCards.get(playerId).getComponents()) { + cardValues += getCardValue(ekgs, card); + } + + return cardValues / (ekgs.playerHandCards.get(playerId).getSize() + 1); + } + + double getCardValue(ExplodingKittensGameState ekgs, ExplodingKittensCard card) { + switch (card.cardType) { + case EXPLODING_KITTEN: + return explodingValue; + case DEFUSE: + return defuseValue; + case NOPE: + if (ekgs.isActionInProgress()) { + return nopeValue; + } else return 0; // Neutral + case ATTACK: + return attackValue; + case SKIP: + return skipValue; + case FAVOR: + return favorValue; + case SHUFFLE: + return shuffleValue; + case SEETHEFUTURE: + return seeFutureValue; + default: + return regularValue; + } + } + + @Override + protected ExplodingKittensHeuristic _copy() { + return new ExplodingKittensHeuristic(); + // copying of parameterisable values is done in the super class + } + + @Override + protected boolean _equals(Object o) { + return o instanceof ExplodingKittensHeuristic; + // checking of parameterisable values is done in the super class + } + + /** + * @return Returns Tuned Parameters corresponding to the current settings + * (will use all defaults if setParameterValue has not been called at all) + */ + @Override + public ExplodingKittensHeuristic instantiate() { + return (ExplodingKittensHeuristic) this.copy(); + } + + +} \ No newline at end of file diff --git a/src/main/java/games/explodingkittensOLD/ExplodingKittensParameters.java b/src/main/java/games/explodingkittensOLD/ExplodingKittensParameters.java new file mode 100644 index 000000000..d9eced0da --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/ExplodingKittensParameters.java @@ -0,0 +1,89 @@ +package games.explodingkittensOLD; + +import core.AbstractParameters; +import core.Game; +import evaluation.optimisation.TunableParameters; +import games.GameType; +import games.explodingkittensOLD.cards.ExplodingKittensCard; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Objects; + +public class ExplodingKittensParameters extends TunableParameters { + + String dataPath = "data/explodingkittens/"; + + HashMap cardCounts = new HashMap() {{ + put(ExplodingKittensCard.CardType.ATTACK, 4); + put(ExplodingKittensCard.CardType.SKIP, 4); + put(ExplodingKittensCard.CardType.FAVOR, 4); + put(ExplodingKittensCard.CardType.SHUFFLE, 4); + put(ExplodingKittensCard.CardType.SEETHEFUTURE, 5); + put(ExplodingKittensCard.CardType.TACOCAT, 4); + put(ExplodingKittensCard.CardType.MELONCAT, 4); + put(ExplodingKittensCard.CardType.BEARDCAT, 4); + put(ExplodingKittensCard.CardType.RAINBOWCAT, 4); + put(ExplodingKittensCard.CardType.FURRYCAT, 4); + put(ExplodingKittensCard.CardType.NOPE, 5); + put(ExplodingKittensCard.CardType.DEFUSE, 6); + put(ExplodingKittensCard.CardType.EXPLODING_KITTEN, -1); + }}; + public int nCardsPerPlayer = 7; + public int nDefuseCards = 6; + public int nSeeFutureCards = 3; + public boolean nopeOwnCards = true; + + public ExplodingKittensParameters() { + addTunableParameter("nCardsPerPlayer", 7, Arrays.asList(3,5,7,10,15)); + addTunableParameter("nDefuseCards", 6, Arrays.asList(1,2,3,6,9)); + addTunableParameter("nSeeFutureCards", 3, Arrays.asList(1,3,5,7)); + addTunableParameter("nopeOwnCards", true, Arrays.asList(false, true)); + for (ExplodingKittensCard.CardType c: cardCounts.keySet()) { + if (c == ExplodingKittensCard.CardType.EXPLODING_KITTEN) addTunableParameter(c.name() + " count", -1); + else addTunableParameter(c.name() + " count", cardCounts.get(c), Arrays.asList(1,2,3,4,5)); + } + _reset(); + } + + @Override + public void _reset() { + nCardsPerPlayer = (int) getParameterValue("nCardsPerPlayer"); + nDefuseCards = (int) getParameterValue("nDefuseCards"); + nSeeFutureCards = (int) getParameterValue("nSeeFutureCards"); + nopeOwnCards = (boolean) getParameterValue("nopeOwnCards"); + cardCounts.replaceAll((c, v) -> (Integer) getParameterValue(c.name() + " count")); + } + + public String getDataPath() { + return dataPath; + } + + @Override + protected AbstractParameters _copy() { + return new ExplodingKittensParameters(); + } + + @Override + protected boolean _equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExplodingKittensParameters)) return false; + if (!super.equals(o)) return false; + ExplodingKittensParameters that = (ExplodingKittensParameters) o; + return nCardsPerPlayer == that.nCardsPerPlayer && + nDefuseCards == that.nDefuseCards && + nSeeFutureCards == that.nSeeFutureCards && + Objects.equals(dataPath, that.dataPath) && + Objects.equals(cardCounts, that.cardCounts); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), dataPath, cardCounts, nCardsPerPlayer, nDefuseCards, nSeeFutureCards); + } + + @Override + public Object instantiate() { + return new Game(GameType.ExplodingKittens, new ExplodingKittensForwardModel(), new ExplodingKittensGameState(this, GameType.ExplodingKittens.getMinPlayers())); + } +} diff --git a/src/main/java/games/explodingkittens/ExplodingKittensTurnOrder.java b/src/main/java/games/explodingkittensOLD/ExplodingKittensTurnOrder.java similarity index 95% rename from src/main/java/games/explodingkittens/ExplodingKittensTurnOrder.java rename to src/main/java/games/explodingkittensOLD/ExplodingKittensTurnOrder.java index 81fca5ecc..5b854da02 100644 --- a/src/main/java/games/explodingkittens/ExplodingKittensTurnOrder.java +++ b/src/main/java/games/explodingkittensOLD/ExplodingKittensTurnOrder.java @@ -1,16 +1,16 @@ -package games.explodingkittens; +package games.explodingkittensOLD; import core.AbstractGameState; import core.CoreConstants; import core.turnorders.ReactiveTurnOrder; import core.turnorders.TurnOrder; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.ArrayList; import java.util.LinkedList; import java.util.Objects; -import static games.explodingkittens.ExplodingKittensGameState.ExplodingKittensGamePhase.Nope; +import static games.explodingkittensOLD.ExplodingKittensGameState.ExplodingKittensGamePhase.Nope; import static core.CoreConstants.GameResult.GAME_ONGOING; public class ExplodingKittensTurnOrder extends ReactiveTurnOrder { diff --git a/src/main/java/games/explodingkittens/actions/AttackAction.java b/src/main/java/games/explodingkittensOLD/actions/AttackAction.java similarity index 91% rename from src/main/java/games/explodingkittens/actions/AttackAction.java rename to src/main/java/games/explodingkittensOLD/actions/AttackAction.java index ae68a13af..777cdfd49 100644 --- a/src/main/java/games/explodingkittens/actions/AttackAction.java +++ b/src/main/java/games/explodingkittensOLD/actions/AttackAction.java @@ -1,13 +1,13 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.actions.AbstractAction; import core.actions.DrawCard; import core.AbstractGameState; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.ExplodingKittensTurnOrder; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.ExplodingKittensTurnOrder; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.Arrays; import java.util.Objects; diff --git a/src/main/java/games/explodingkittens/actions/DrawExplodingKittenCard.java b/src/main/java/games/explodingkittensOLD/actions/DrawExplodingKittenCard.java similarity index 88% rename from src/main/java/games/explodingkittens/actions/DrawExplodingKittenCard.java rename to src/main/java/games/explodingkittensOLD/actions/DrawExplodingKittenCard.java index cb48bae60..d8479eec8 100644 --- a/src/main/java/games/explodingkittens/actions/DrawExplodingKittenCard.java +++ b/src/main/java/games/explodingkittensOLD/actions/DrawExplodingKittenCard.java @@ -1,15 +1,15 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.actions.AbstractAction; import core.actions.DrawCard; import core.AbstractGameState; import core.components.Deck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensTurnOrder; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensTurnOrder; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.cards.ExplodingKittensCard; -import static games.explodingkittens.ExplodingKittensGameState.ExplodingKittensGamePhase.Defuse; +import static games.explodingkittensOLD.ExplodingKittensGameState.ExplodingKittensGamePhase.Defuse; public class DrawExplodingKittenCard extends DrawCard implements IPrintable { diff --git a/src/main/java/games/explodingkittens/actions/FavorAction.java b/src/main/java/games/explodingkittensOLD/actions/FavorAction.java similarity index 88% rename from src/main/java/games/explodingkittens/actions/FavorAction.java rename to src/main/java/games/explodingkittensOLD/actions/FavorAction.java index d93dab24a..99e99a454 100644 --- a/src/main/java/games/explodingkittens/actions/FavorAction.java +++ b/src/main/java/games/explodingkittensOLD/actions/FavorAction.java @@ -1,18 +1,18 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.actions.AbstractAction; import core.actions.DrawCard; import core.AbstractGameState; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensTurnOrder; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensTurnOrder; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.Arrays; import java.util.Objects; -import static games.explodingkittens.ExplodingKittensGameState.ExplodingKittensGamePhase.Favor; +import static games.explodingkittensOLD.ExplodingKittensGameState.ExplodingKittensGamePhase.Favor; public class FavorAction extends DrawCard implements IsNopeable, IPrintable { final int target; diff --git a/src/main/java/games/explodingkittens/actions/IsNopeable.java b/src/main/java/games/explodingkittensOLD/actions/IsNopeable.java similarity index 78% rename from src/main/java/games/explodingkittens/actions/IsNopeable.java rename to src/main/java/games/explodingkittensOLD/actions/IsNopeable.java index e59ed28ed..a08227534 100644 --- a/src/main/java/games/explodingkittens/actions/IsNopeable.java +++ b/src/main/java/games/explodingkittensOLD/actions/IsNopeable.java @@ -1,4 +1,4 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.AbstractGameState; diff --git a/src/main/java/games/explodingkittens/actions/NopeAction.java b/src/main/java/games/explodingkittensOLD/actions/NopeAction.java similarity index 96% rename from src/main/java/games/explodingkittens/actions/NopeAction.java rename to src/main/java/games/explodingkittensOLD/actions/NopeAction.java index 952a25971..c31dabdd6 100644 --- a/src/main/java/games/explodingkittens/actions/NopeAction.java +++ b/src/main/java/games/explodingkittensOLD/actions/NopeAction.java @@ -1,4 +1,4 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.AbstractGameState; import core.actions.AbstractAction; diff --git a/src/main/java/games/explodingkittens/actions/SeeTheFuture.java b/src/main/java/games/explodingkittensOLD/actions/SeeTheFuture.java similarity index 88% rename from src/main/java/games/explodingkittens/actions/SeeTheFuture.java rename to src/main/java/games/explodingkittensOLD/actions/SeeTheFuture.java index 97bfd28b4..4fe57ed4b 100644 --- a/src/main/java/games/explodingkittens/actions/SeeTheFuture.java +++ b/src/main/java/games/explodingkittensOLD/actions/SeeTheFuture.java @@ -1,17 +1,17 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.actions.AbstractAction; import core.AbstractGameState; import core.actions.DrawCard; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.ExplodingKittensParameters; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.ExplodingKittensParameters; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.Arrays; -import static games.explodingkittens.ExplodingKittensGameState.ExplodingKittensGamePhase.SeeTheFuture; +import static games.explodingkittensOLD.ExplodingKittensGameState.ExplodingKittensGamePhase.SeeTheFuture; public class SeeTheFuture extends DrawCard implements IsNopeable, IPrintable { diff --git a/src/main/java/games/explodingkittens/actions/ShuffleAction.java b/src/main/java/games/explodingkittensOLD/actions/ShuffleAction.java similarity index 91% rename from src/main/java/games/explodingkittens/actions/ShuffleAction.java rename to src/main/java/games/explodingkittensOLD/actions/ShuffleAction.java index 599c6a318..abd63b2e9 100644 --- a/src/main/java/games/explodingkittens/actions/ShuffleAction.java +++ b/src/main/java/games/explodingkittensOLD/actions/ShuffleAction.java @@ -1,15 +1,14 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.actions.AbstractAction; import core.actions.DrawCard; import core.AbstractGameState; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.Arrays; -import java.util.Random; public class ShuffleAction extends DrawCard implements IsNopeable, IPrintable { diff --git a/src/main/java/games/explodingkittens/actions/SkipAction.java b/src/main/java/games/explodingkittensOLD/actions/SkipAction.java similarity index 90% rename from src/main/java/games/explodingkittens/actions/SkipAction.java rename to src/main/java/games/explodingkittensOLD/actions/SkipAction.java index 4a44dc442..3a07b73ca 100644 --- a/src/main/java/games/explodingkittens/actions/SkipAction.java +++ b/src/main/java/games/explodingkittensOLD/actions/SkipAction.java @@ -1,4 +1,4 @@ -package games.explodingkittens.actions; +package games.explodingkittensOLD.actions; import core.CoreConstants; import core.actions.AbstractAction; @@ -6,9 +6,9 @@ import core.AbstractGameState; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.ExplodingKittensTurnOrder; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.ExplodingKittensTurnOrder; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.Arrays; diff --git a/src/main/java/games/explodingkittens/actions/reactions/ChooseSeeTheFutureOrder.java b/src/main/java/games/explodingkittensOLD/actions/reactions/ChooseSeeTheFutureOrder.java similarity index 91% rename from src/main/java/games/explodingkittens/actions/reactions/ChooseSeeTheFutureOrder.java rename to src/main/java/games/explodingkittensOLD/actions/reactions/ChooseSeeTheFutureOrder.java index d2eb8a9cf..1a7751eca 100644 --- a/src/main/java/games/explodingkittens/actions/reactions/ChooseSeeTheFutureOrder.java +++ b/src/main/java/games/explodingkittensOLD/actions/reactions/ChooseSeeTheFutureOrder.java @@ -1,4 +1,4 @@ -package games.explodingkittens.actions.reactions; +package games.explodingkittensOLD.actions.reactions; import core.AbstractGameState; import core.CoreConstants; @@ -6,8 +6,8 @@ import core.actions.RearrangeDeckOfCards; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.cards.ExplodingKittensCard; import java.util.Arrays; diff --git a/src/main/java/games/explodingkittens/actions/reactions/GiveCard.java b/src/main/java/games/explodingkittensOLD/actions/reactions/GiveCard.java similarity index 88% rename from src/main/java/games/explodingkittens/actions/reactions/GiveCard.java rename to src/main/java/games/explodingkittensOLD/actions/reactions/GiveCard.java index 9c9ab825d..ff47f9a6f 100644 --- a/src/main/java/games/explodingkittens/actions/reactions/GiveCard.java +++ b/src/main/java/games/explodingkittensOLD/actions/reactions/GiveCard.java @@ -1,4 +1,4 @@ -package games.explodingkittens.actions.reactions; +package games.explodingkittensOLD.actions.reactions; import core.CoreConstants; import core.actions.AbstractAction; @@ -7,9 +7,9 @@ import core.components.Card; import core.components.Deck; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensTurnOrder; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.cards.ExplodingKittensCard; +import games.explodingkittensOLD.ExplodingKittensTurnOrder; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.cards.ExplodingKittensCard; public class GiveCard extends DrawCard implements IPrintable { diff --git a/src/main/java/games/explodingkittens/actions/reactions/PassAction.java b/src/main/java/games/explodingkittensOLD/actions/reactions/PassAction.java similarity index 92% rename from src/main/java/games/explodingkittens/actions/reactions/PassAction.java rename to src/main/java/games/explodingkittensOLD/actions/reactions/PassAction.java index eb99930a1..8463a0a63 100644 --- a/src/main/java/games/explodingkittens/actions/reactions/PassAction.java +++ b/src/main/java/games/explodingkittensOLD/actions/reactions/PassAction.java @@ -1,4 +1,4 @@ -package games.explodingkittens.actions.reactions; +package games.explodingkittensOLD.actions.reactions; import core.AbstractGameState; import core.actions.AbstractAction; diff --git a/src/main/java/games/explodingkittens/actions/reactions/PlaceExplodingKitten.java b/src/main/java/games/explodingkittensOLD/actions/reactions/PlaceExplodingKitten.java similarity index 90% rename from src/main/java/games/explodingkittens/actions/reactions/PlaceExplodingKitten.java rename to src/main/java/games/explodingkittensOLD/actions/reactions/PlaceExplodingKitten.java index 65729fee9..c3792d9a9 100644 --- a/src/main/java/games/explodingkittens/actions/reactions/PlaceExplodingKitten.java +++ b/src/main/java/games/explodingkittensOLD/actions/reactions/PlaceExplodingKitten.java @@ -1,12 +1,12 @@ -package games.explodingkittens.actions.reactions; +package games.explodingkittensOLD.actions.reactions; import core.AbstractGameState; import core.CoreConstants; import core.actions.AbstractAction; import core.actions.DrawCard; import core.interfaces.IPrintable; -import games.explodingkittens.ExplodingKittensGameState; -import games.explodingkittens.ExplodingKittensTurnOrder; +import games.explodingkittensOLD.ExplodingKittensGameState; +import games.explodingkittensOLD.ExplodingKittensTurnOrder; public class PlaceExplodingKitten extends DrawCard implements IPrintable { diff --git a/src/main/java/games/explodingkittensOLD/cards/ExplodingKittensCard.java b/src/main/java/games/explodingkittensOLD/cards/ExplodingKittensCard.java new file mode 100644 index 000000000..f0e29d2ec --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/cards/ExplodingKittensCard.java @@ -0,0 +1,43 @@ +package games.explodingkittensOLD.cards; + +import core.components.Card; + +public class ExplodingKittensCard extends Card { + public enum CardType { + EXPLODING_KITTEN, + DEFUSE, + NOPE, + ATTACK, + SKIP, + FAVOR, + SHUFFLE, + SEETHEFUTURE, + TACOCAT, + MELONCAT, + FURRYCAT, + BEARDCAT, + RAINBOWCAT, + } + + public CardType cardType; + + public ExplodingKittensCard(CardType cardType) { + super(cardType.toString()); + this.cardType = cardType; + } + + public ExplodingKittensCard(CardType cardType, int ID) { + super(cardType.toString(), ID); + this.cardType = cardType; + } + + @Override + public Card copy() { + return new ExplodingKittensCard(cardType, componentID); + } + + @Override + public String toString() { + return cardType.name(); + } +} diff --git a/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensDeckView.java b/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensDeckView.java new file mode 100644 index 000000000..767fe6a99 --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensDeckView.java @@ -0,0 +1,96 @@ +package games.explodingkittensOLD.gui; + +import core.components.Deck; +import games.explodingkittensOLD.cards.ExplodingKittensCard; +import gui.views.CardView; +import gui.views.DeckView; +import utilities.ImageIO; + +import java.awt.*; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Random; + +import static games.explodingkittensOLD.gui.ExplodingKittensGUIManager.*; + + +public class ExplodingKittensDeckView extends DeckView { + + // Back of card image + Image backOfCard; + // Path to assets + String dataPath; + + // Card images + HashMap cardCatImageMapping; + ArrayList catImages; + Random rnd = new Random(); // This doesn't need to use the game random seed + + /** + * Constructor initialising information and adding key/mouse listener for card highlight (left click or ALT + hover + * allows showing the highlighted card on top of all others). + * @param d - deck to draw + * @param visible - true if whole deck visible + * @param dataPath - path to assets + */ + public ExplodingKittensDeckView(int player, Deck d, boolean visible, String dataPath) { + super(player, d, visible, ekCardWidth, ekCardHeight, new Rectangle(5, 5, playerAreaWidth, playerAreaHeight)); + backOfCard = ImageIO.GetInstance().getImage(dataPath + "CardBack.png"); + this.dataPath = dataPath; + cardCatImageMapping = new HashMap<>(); + + // Get card Images + File dir = new File(dataPath + "cats/"); + File[] files = dir.listFiles(); + catImages = new ArrayList<>(); + if (files != null) { + for (File f : files) { + catImages.add(f.getAbsolutePath()); + } + } + } + + /** + * Draws the specified component at the specified place + * + * @param g Graphics object + * @param rect Where the item is to be drawn + * @param card The item itself + * @param visible true if the item is visible (e.g. the card details); false if only the card-back + */ + @Override + public void drawComponent(Graphics2D g, Rectangle rect, ExplodingKittensCard card, boolean visible) { + drawCat(g, card, rect, visible); + } + + /** + * Draws an Exploding Kittens card, with a random cat icon. + * @param g - Graphics object + * @param card - card to draw on + * @param r - rectangle in which card is to be drawn + * @param visible - if the card is visible or not + */ + private void drawCat(Graphics2D g, ExplodingKittensCard card, Rectangle r, boolean visible) { + Image cardFace = ImageIO.GetInstance().getImage(dataPath + card.cardType.name().toLowerCase() + ".png"); + CardView.drawCard(g, r, card, cardFace, backOfCard, visible); + + if (visible) { + // Draw decorative cat image if card is visible + Image catImg; + if (cardCatImageMapping.containsKey(card.getComponentID())) { + catImg = cardCatImageMapping.get(card.getComponentID()); + } else { + // Random selection from those available + int choice = rnd.nextInt(catImages.size()); + catImg = ImageIO.GetInstance().getImage(catImages.get(choice)); + catImages.remove(choice); + cardCatImageMapping.put(card.getComponentID(), catImg); + } + double scaleW = 1.0 * defaultItemSize / catImg.getWidth(null); + int height = (int) (catImg.getHeight(null) * scaleW); + g.drawImage(catImg, r.x + ekCardWidth / 2 - defaultItemSize / 2, r.y + ekCardHeight / 2, defaultItemSize, height, null); + } + } + +} diff --git a/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensDiscardView.java b/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensDiscardView.java new file mode 100644 index 000000000..33d0a0bb1 --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensDiscardView.java @@ -0,0 +1,78 @@ +package games.explodingkittensOLD.gui; + +import core.actions.AbstractAction; +import core.components.Component; +import core.components.Deck; +import games.explodingkittensOLD.actions.*; +import games.explodingkittensOLD.actions.reactions.ChooseSeeTheFutureOrder; +import games.explodingkittensOLD.cards.ExplodingKittensCard; + +import java.awt.*; +import java.util.*; + +public class ExplodingKittensDiscardView extends ExplodingKittensDeckView { + + // Images for Action Stack + ArrayList catImagesBkup; + + // This view adds action stack as cards in the discard pile for display + Stack actionStack; + + /** + * Constructor initialising information and adding key/mouse listener for card highlight (left click or ALT + hover + * allows showing the highlighted card on top of all others). + * @param d - deck to draw + * @param visible - true if whole deck visible + * @param dataPath - path to assets + */ + public ExplodingKittensDiscardView(Deck d, Stack actionStack, boolean visible, String dataPath) { + super(-1, d, visible, dataPath); + this.actionStack = actionStack; + catImagesBkup = new ArrayList<>(); + catImagesBkup.addAll(catImages); + } + + /** + * In the case of ExplodingKittens Discard we also display the contents of the action stack + * @param g - Graphics object + */ + public void drawDeck(Graphics2D g) { + Component oldComponent = component; + @SuppressWarnings("unchecked") Deck deckCopy = ((Deck) component).copy(); + + // Add cards played from action stack into the copy of the deck + for (AbstractAction aa: actionStack) { + ExplodingKittensCard card = getStackCard(aa); + if (card != null) { + deckCopy.add(card); + } + } + // set thie copy to tbe the component we draw + component = deckCopy; + // and draw it + super.drawDeck(g); + // then reset to the original + component = oldComponent; + } + + /** + * Turns an action into a card + * @param aa - action to turn to card + * @return - Exploding kittens card + */ + private ExplodingKittensCard getStackCard(AbstractAction aa) { + if (aa instanceof AttackAction) { + return new ExplodingKittensCard(ExplodingKittensCard.CardType.ATTACK, 0); + } else if (aa instanceof ChooseSeeTheFutureOrder) { + return new ExplodingKittensCard(ExplodingKittensCard.CardType.FAVOR, 1); + } else if (aa instanceof NopeAction) { + return new ExplodingKittensCard(ExplodingKittensCard.CardType.SEETHEFUTURE, 3); + } else if (aa instanceof ShuffleAction) { + return new ExplodingKittensCard(ExplodingKittensCard.CardType.SHUFFLE, 4); + } else if (aa instanceof SkipAction) { + return new ExplodingKittensCard(ExplodingKittensCard.CardType.SKIP, 5); + } + return null; + } + +} diff --git a/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensGUIManager.java b/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensGUIManager.java new file mode 100644 index 000000000..528bffb0d --- /dev/null +++ b/src/main/java/games/explodingkittensOLD/gui/ExplodingKittensGUIManager.java @@ -0,0 +1,167 @@ +package games.explodingkittensOLD.gui; + +import gui.AbstractGUIManager; +import gui.GamePanel; +import core.AbstractGameState; +import core.AbstractPlayer; +import core.Game; +import games.explodingkittensOLD.ExplodingKittensParameters; +import games.explodingkittensOLD.ExplodingKittensGameState; +import gui.IScreenHighlight; +import players.human.ActionController; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.EtchedBorder; +import javax.swing.border.TitledBorder; +import java.awt.*; +import java.util.Set; + + +public class ExplodingKittensGUIManager extends AbstractGUIManager { + // Settings for display areas + final static int playerAreaWidth = 300; + final static int playerAreaHeight = 135; + final static int ekCardWidth = 90; + final static int ekCardHeight = 110; + + // List of player hand views + ExplodingKittensDeckView[] playerHands; + // Discard pile view + ExplodingKittensDiscardView discardPile; + // Draw pile view + ExplodingKittensDeckView drawPile; + + // Currently active player + int activePlayer = -1; + // Border highlight of active player + Border highlightActive = BorderFactory.createLineBorder(new Color(220, 169, 11), 3); + Border[] playerViewBorders; + + public ExplodingKittensGUIManager(GamePanel parent, Game game, ActionController ac, Set humanID) { + super(parent, game, ac, humanID); + + if (game != null) { + AbstractGameState gameState = game.getGameState(); + if (gameState != null) { + // Initialise active player + activePlayer = gameState.getCurrentPlayer(); + + // Find required size of window + int nPlayers = gameState.getNPlayers(); + int nHorizAreas = 1 + (nPlayers <= 3 ? 2 : nPlayers == 4 ? 3 : nPlayers <= 8 ? 4 : 5); + double nVertAreas = 5; + this.width = playerAreaWidth * nHorizAreas; + this.height = (int) (playerAreaHeight * nVertAreas) + 20; + + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; + ExplodingKittensParameters ekgp = (ExplodingKittensParameters) gameState.getGameParameters(); + + // Create main game area that will hold all game views + playerHands = new ExplodingKittensDeckView[nPlayers]; + playerViewBorders = new Border[nPlayers]; + JPanel mainGameArea = new JPanel(); + mainGameArea.setLayout(new BorderLayout()); + + // Player hands go on the edges + String[] locations = new String[]{BorderLayout.NORTH, BorderLayout.EAST, BorderLayout.SOUTH, BorderLayout.WEST}; + JPanel[] sides = new JPanel[]{new JPanel(), new JPanel(), new JPanel(), new JPanel()}; + int next = 0; + for (int i = 0; i < nPlayers; i++) { + ExplodingKittensDeckView playerHand = new ExplodingKittensDeckView(i, ekgs.getPlayerHandCards().get(i), false, ekgp.getDataPath()); + + // Get agent name + String[] split = game.getPlayers().get(i).getClass().toString().split("\\."); + String agentName = split[split.length - 1]; + + // Create border, layouts and keep track of this view + TitledBorder title = BorderFactory.createTitledBorder( + BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), "Player " + i + " [" + agentName + "]", + TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM); + playerViewBorders[i] = title; + playerHand.setBorder(title); + sides[next].add(playerHand); + sides[next].setLayout(new GridBagLayout()); + next = (next + 1) % (locations.length); + playerHands[i] = playerHand; + } + for (int i = 0; i < locations.length; i++) { + mainGameArea.add(sides[i], locations[i]); + } + + // Discard and draw piles go in the center + JPanel centerArea = new JPanel(); + centerArea.setLayout(new BoxLayout(centerArea, BoxLayout.Y_AXIS)); + discardPile = new ExplodingKittensDiscardView(ekgs.getDiscardPile(), ekgs.getActionStack(), true, ekgp.getDataPath()); + drawPile = new ExplodingKittensDeckView(-1, ekgs.getDrawPile(), gameState.getCoreGameParameters().alwaysDisplayFullObservable, ekgp.getDataPath()); + centerArea.add(drawPile); + centerArea.add(discardPile); + JPanel jp = new JPanel(); + jp.setLayout(new GridBagLayout()); + jp.add(centerArea); + mainGameArea.add(jp, BorderLayout.CENTER); + + // Top area will show state information + JPanel infoPanel = createGameStateInfoPanel("Exploding Kittens", gameState, width, defaultInfoPanelHeight); + // Bottom area will show actions available + JComponent actionPanel = createActionPanel(new IScreenHighlight[0], width, defaultActionPanelHeight, false, true, null, null, null); + + // Add all views to frame + parent.setLayout(new BorderLayout()); + parent.add(mainGameArea, BorderLayout.CENTER); + parent.add(infoPanel, BorderLayout.NORTH); + parent.add(actionPanel, BorderLayout.SOUTH); + + parent.revalidate(); + parent.setVisible(true); + parent.repaint(); + } + } + + } + + @Override + public int getMaxActionSpace() { + return 25; + } + + @Override + protected void _update(AbstractPlayer player, AbstractGameState gameState) { + if (gameState != null) { + if (gameState.getCurrentPlayer() != activePlayer) { + playerHands[activePlayer].setCardHighlight(-1); + activePlayer = gameState.getCurrentPlayer(); + } + + // Update decks and visibility + ExplodingKittensGameState ekgs = (ExplodingKittensGameState) gameState; + for (int i = 0; i < gameState.getNPlayers(); i++) { + playerHands[i].updateComponent(ekgs.getPlayerHandCards().get(i)); + if (i == gameState.getCurrentPlayer() && gameState.getCoreGameParameters().alwaysDisplayCurrentPlayer + || humanPlayerIds.contains(i) + || gameState.getCoreGameParameters().alwaysDisplayFullObservable) { + playerHands[i].setFront(true); + playerHands[i].setFocusable(true); + } else { + playerHands[i].setFront(false); + } + + // Highlight active player + if (i == gameState.getCurrentPlayer()) { + Border compound = BorderFactory.createCompoundBorder( + highlightActive, playerViewBorders[i]); + playerHands[i].setBorder(compound); + } else { + playerHands[i].setBorder(playerViewBorders[i]); + } + } + discardPile.updateComponent(ekgs.getDiscardPile()); + discardPile.setFocusable(true); + drawPile.updateComponent(ekgs.getDrawPile()); + if (humanPlayerIds.contains(activePlayer) || gameState.getCoreGameParameters().alwaysDisplayFullObservable) + drawPile.setFront(true); + + } + } + +} diff --git a/src/main/java/games/hearts/HeartsGameState.java b/src/main/java/games/hearts/HeartsGameState.java index 25b5e77c6..0b434acb4 100644 --- a/src/main/java/games/hearts/HeartsGameState.java +++ b/src/main/java/games/hearts/HeartsGameState.java @@ -67,6 +67,9 @@ public Deck getDrawDeck() { return drawDeck; } + /** + * returns a List in playerID order of the player hands + */ public List> getPlayerDecks() { return playerDecks; } @@ -110,6 +113,7 @@ else if (card.equals(params.qosCard)) { } } + // The player's points from all previous hands public int getPlayerPoints(int playerID) { return playerPoints.getOrDefault(playerID, 0); } @@ -193,10 +197,7 @@ protected double _getHeuristicScore(int playerId) { } /** - * For Hearts a lower score is better than a high one. So we return the negative of the player's score. - * - * @param playerId - player observing the state. - * @return + * For Hearts a lower score is better than a high one. So we return the negative of the player's points. */ @Override public double getGameScore(int playerId) { @@ -247,6 +248,10 @@ public int hashCode() { return result; } + /** + * A player's trick deck is the Deck of cards they have won (only the ones with points) in + * previous tricks this round. + */ public List> getPlayerTrickDecks() { return trickDecks; } diff --git a/src/main/java/games/loveletter/LoveLetterForwardModel.java b/src/main/java/games/loveletter/LoveLetterForwardModel.java index dd8222ac1..2347b6902 100644 --- a/src/main/java/games/loveletter/LoveLetterForwardModel.java +++ b/src/main/java/games/loveletter/LoveLetterForwardModel.java @@ -8,6 +8,7 @@ import core.interfaces.ITreeActionSpace; import games.GameType; import games.loveletter.actions.PlayCard; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import utilities.ActionTreeNode; @@ -58,7 +59,7 @@ private void setupRound(LoveLetterGameState llgs, Set previousWinners) // Add all cards to the draw pile llgs.drawPile.clear(); - for (HashMap.Entry entry : llp.cardCounts.entrySet()) { + for (HashMap.Entry entry : llp.cardCounts.entrySet()) { for (int i = 0; i < entry.getValue(); i++) { LoveLetterCard card = new LoveLetterCard(entry.getKey()); llgs.drawPile.add(card); @@ -326,19 +327,19 @@ public List _computeAvailableActions(AbstractGameState gameState Deck playerDeck = llgs.playerHandCards.get(playerID); // in case a player holds the countess and either the king or the prince, the countess needs to be played - LoveLetterCard.CardType cardTypeForceCountess = llgs.needToForceCountess(playerDeck); + CardType cardTypeForceCountess = llgs.needToForceCountess(playerDeck); // We create the respective actions for each card on the player's hand for (int card = 0; card < playerDeck.getSize(); card++) { - LoveLetterCard.CardType cardType = playerDeck.getComponents().get(card).cardType; - if (cardType != LoveLetterCard.CardType.Countess && cardTypeForceCountess != null) continue; + CardType cardType = playerDeck.getComponents().get(card).cardType; + if (cardType != CardType.Countess && cardTypeForceCountess != null) continue; int cardIdx; if (actionSpace.context == ActionSpace.Context.Dependent) cardIdx = card; else cardIdx = -1; // Independent and default if (actionSpace.structure == ActionSpace.Structure.Flat || actionSpace.structure == ActionSpace.Structure.Default) { - actions.addAll(cardType.getFlatActions(llgs, cardIdx, playerID, true)); + actions.addAll(cardType.flatActions(llgs, cardIdx, playerID, true)); } else if (actionSpace.structure == ActionSpace.Structure.Deep) { - actions.addAll(cardType.getDeepActions(llgs, cardIdx, playerID, true)); + actions.addAll(cardType.deepActions(llgs, cardIdx, playerID, true)); } } @@ -355,10 +356,10 @@ public ActionTreeNode updateActionTree(ActionTreeNode root, AbstractGameState ga PlayCard llAction = (PlayCard) action; // TODO - Probaly a better way to get the card names - LoveLetterCard.CardType[] soloCards = new LoveLetterCard.CardType[]{ - LoveLetterCard.CardType.Handmaid, - LoveLetterCard.CardType.Countess, - LoveLetterCard.CardType.Princess + CardType[] soloCards = new CardType[]{ + CardType.Handmaid, + CardType.Countess, + CardType.Princess }; // Actions stored in the card type layer (Layer 1) @@ -370,7 +371,7 @@ public ActionTreeNode updateActionTree(ActionTreeNode root, AbstractGameState ga else { ActionTreeNode cardNode = root.findChildrenByName(llAction.getCardType().toString().toLowerCase()); ActionTreeNode playerNode = cardNode.findChildrenByName("player" + llAction.getTargetPlayer()); - if (llAction.getCardType() == LoveLetterCard.CardType.Guard) { + if (llAction.getCardType() == CardType.Guard) { if (llAction.getTargetCardType() == null) { playerNode.getChildren().get(0).setAction(action); } else { diff --git a/src/main/java/games/loveletter/LoveLetterGameState.java b/src/main/java/games/loveletter/LoveLetterGameState.java index ae08b65d5..b595e69c6 100644 --- a/src/main/java/games/loveletter/LoveLetterGameState.java +++ b/src/main/java/games/loveletter/LoveLetterGameState.java @@ -9,6 +9,7 @@ import core.interfaces.IPrintable; import evaluation.metrics.Event; import games.GameType; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import java.util.*; @@ -116,6 +117,7 @@ protected double _getHeuristicScore(int playerId) { } @Override + /** Returns the score of the player (the number of affection tokens) */ public double getGameScore(int playerId) { return affectionTokens[playerId]; } @@ -156,10 +158,10 @@ void updateComponents() { * @param playerDeck - deck of player to check * @return - card type of the card that forces the countess to be played, null if countess not forced */ - public LoveLetterCard.CardType needToForceCountess(Deck playerDeck) { + public CardType needToForceCountess(Deck playerDeck) { boolean ownsCountess = false; for (LoveLetterCard card : playerDeck.getComponents()) { - if (card.cardType == LoveLetterCard.CardType.Countess) { + if (card.cardType == CardType.Countess) { ownsCountess = true; break; } @@ -167,7 +169,7 @@ public LoveLetterCard.CardType needToForceCountess(Deck playerDe if (ownsCountess) { for (LoveLetterCard card : playerDeck.getComponents()) { - if (card.cardType == LoveLetterCard.CardType.Prince || card.cardType == LoveLetterCard.CardType.King) { + if (card.cardType == CardType.Prince || card.cardType == CardType.King) { return card.cardType; } } @@ -182,7 +184,7 @@ public LoveLetterCard.CardType needToForceCountess(Deck playerDe * @param targetPlayer - ID of player killed * @param cardType - card used to kill */ - public void killPlayer(int whoKill, int targetPlayer, LoveLetterCard.CardType cardType) { + public void killPlayer(int whoKill, int targetPlayer, CardType cardType) { setPlayerResult(CoreConstants.GameResult.LOSE_ROUND, targetPlayer); // a losing player needs to discard all cards @@ -201,6 +203,7 @@ public Deck getReserveCards() { return reserveCards; } + /** Player is protected by the Handmaid */ public boolean isProtected(int playerID) { return effectProtection[playerID]; } @@ -209,6 +212,7 @@ public void setProtection(int playerID, boolean protection) { effectProtection[playerID] = protection; } + /** Number of cards left in the draw deck */ public int getRemainingCards() { return drawPile.getSize(); } @@ -225,6 +229,10 @@ public PartialObservableDeck getDrawPile() { return drawPile; } + /** + * Returns the affection tokens for each player in an array. + * The index of the array corresponds to the player ID. + */ public int[] getAffectionTokens() { return affectionTokens; } diff --git a/src/main/java/games/loveletter/LoveLetterHeuristic.java b/src/main/java/games/loveletter/LoveLetterHeuristic.java index f483307ba..08418f5c2 100644 --- a/src/main/java/games/loveletter/LoveLetterHeuristic.java +++ b/src/main/java/games/loveletter/LoveLetterHeuristic.java @@ -5,6 +5,7 @@ import core.components.PartialObservableDeck; import core.interfaces.IStateHeuristic; import evaluation.optimisation.TunableParameters; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import utilities.Utils; @@ -12,7 +13,7 @@ import java.util.Set; import java.util.stream.IntStream; -import static games.loveletter.cards.LoveLetterCard.CardType.*; +import static games.loveletter.cards.CardType.*; public class LoveLetterHeuristic extends TunableParameters implements IStateHeuristic { @@ -70,7 +71,7 @@ public double evaluateState(AbstractGameState gs, int playerId) { } double cardValues = 0; - Set cardTypes = new HashSet<>(); + Set cardTypes = new HashSet<>(); for (LoveLetterCard card : llgs.getPlayerHandCards().get(playerId).getComponents()) { cardValues += card.cardType.getValue(); cardTypes.add(card.cardType); diff --git a/src/main/java/games/loveletter/LoveLetterParameters.java b/src/main/java/games/loveletter/LoveLetterParameters.java index 1c60dabdd..696faf42b 100644 --- a/src/main/java/games/loveletter/LoveLetterParameters.java +++ b/src/main/java/games/loveletter/LoveLetterParameters.java @@ -4,7 +4,7 @@ import core.Game; import evaluation.optimisation.TunableParameters; import games.GameType; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.Arrays; import java.util.HashMap; @@ -18,15 +18,15 @@ public class LoveLetterParameters extends TunableParameters { String dataPath = "data/loveletter/"; // Occurrence count for each card - public HashMap cardCounts = new HashMap() {{ - put(LoveLetterCard.CardType.Princess, 1); - put(LoveLetterCard.CardType.Countess, 1); - put(LoveLetterCard.CardType.King, 1); - put(LoveLetterCard.CardType.Prince, 2); - put(LoveLetterCard.CardType.Handmaid, 2); - put(LoveLetterCard.CardType.Baron, 2); - put(LoveLetterCard.CardType.Priest, 2); - put(LoveLetterCard.CardType.Guard, 5); + public HashMap cardCounts = new HashMap() {{ + put(CardType.Princess, 1); + put(CardType.Countess, 1); + put(CardType.King, 1); + put(CardType.Prince, 2); + put(CardType.Handmaid, 2); + put(CardType.Baron, 2); + put(CardType.Priest, 2); + put(CardType.Guard, 5); }}; // How many cards each player draws @@ -44,7 +44,7 @@ public LoveLetterParameters() { addTunableParameter("nTokensWin2", 7, Arrays.asList(3,4,5,6,7,8,9,10)); addTunableParameter("nTokensWin3", 5, Arrays.asList(3,4,5,6,7,8,9,10)); addTunableParameter("nTokensWin4", 4, Arrays.asList(3,4,5,6,7,8,9,10)); - for (LoveLetterCard.CardType c: cardCounts.keySet()) { + for (CardType c: cardCounts.keySet()) { addTunableParameter(c.name() + " count", cardCounts.get(c), Arrays.asList(1,2,3,4,5)); } _reset(); diff --git a/src/main/java/games/loveletter/actions/BaronAction.java b/src/main/java/games/loveletter/actions/BaronAction.java index 54cba9334..7669e8921 100644 --- a/src/main/java/games/loveletter/actions/BaronAction.java +++ b/src/main/java/games/loveletter/actions/BaronAction.java @@ -5,17 +5,18 @@ import core.components.PartialObservableDeck; import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; /** * The Baron lets two players compare their hand card. The player with the lesser valued card is removed from the game. */ public class BaronAction extends PlayCard implements IPrintable { - private transient LoveLetterCard.CardType playerCard; - private transient LoveLetterCard.CardType opponentCard; + private transient CardType playerCard; + private transient CardType opponentCard; public BaronAction(int cardIdx, int playerID, int opponentID, boolean canExecuteEffect, boolean discard) { - super(LoveLetterCard.CardType.Baron, cardIdx, playerID, opponentID, null, null, canExecuteEffect, discard); + super(CardType.Baron, cardIdx, playerID, opponentID, null, null, canExecuteEffect, discard); } @Override diff --git a/src/main/java/games/loveletter/actions/GuardAction.java b/src/main/java/games/loveletter/actions/GuardAction.java index f0edb210b..b9d70453a 100644 --- a/src/main/java/games/loveletter/actions/GuardAction.java +++ b/src/main/java/games/loveletter/actions/GuardAction.java @@ -4,6 +4,7 @@ import core.components.Deck; import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; /** @@ -12,8 +13,8 @@ */ public class GuardAction extends PlayCard implements IPrintable { - public GuardAction(int cardIdx, int playerID, int opponentID, LoveLetterCard.CardType cardtype, boolean canExecuteEffect, boolean discard) { - super(LoveLetterCard.CardType.Guard, cardIdx, playerID, opponentID, cardtype, null, canExecuteEffect, discard); + public GuardAction(int cardIdx, int playerID, int opponentID, CardType cardtype, boolean canExecuteEffect, boolean discard) { + super(CardType.Guard, cardIdx, playerID, opponentID, cardtype, null, canExecuteEffect, discard); } @Override diff --git a/src/main/java/games/loveletter/actions/HandmaidAction.java b/src/main/java/games/loveletter/actions/HandmaidAction.java index 87a8aff7d..9567c12bb 100644 --- a/src/main/java/games/loveletter/actions/HandmaidAction.java +++ b/src/main/java/games/loveletter/actions/HandmaidAction.java @@ -1,9 +1,8 @@ package games.loveletter.actions; -import core.AbstractGameState; import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; /** * The handmaid protects the player from any targeted effects until the next turn. @@ -11,7 +10,7 @@ public class HandmaidAction extends PlayCard implements IPrintable { public HandmaidAction(int cardIdx, int playerID) { - super(LoveLetterCard.CardType.Handmaid, cardIdx, playerID, -1, null, null, true, true); + super(CardType.Handmaid, cardIdx, playerID, -1, null, null, true, true); } @Override diff --git a/src/main/java/games/loveletter/actions/KingAction.java b/src/main/java/games/loveletter/actions/KingAction.java index fd0cce54c..0711d379c 100644 --- a/src/main/java/games/loveletter/actions/KingAction.java +++ b/src/main/java/games/loveletter/actions/KingAction.java @@ -5,6 +5,7 @@ import core.components.Deck; import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; @@ -14,7 +15,7 @@ public class KingAction extends PlayCard implements IPrintable { public KingAction(int cardidx, int playerID, int opponentID, boolean canExecuteEffect, boolean discard) { - super(LoveLetterCard.CardType.King, cardidx, playerID, opponentID, null, null, canExecuteEffect, discard); + super(CardType.King, cardidx, playerID, opponentID, null, null, canExecuteEffect, discard); } @Override diff --git a/src/main/java/games/loveletter/actions/PlayCard.java b/src/main/java/games/loveletter/actions/PlayCard.java index 2b48cd519..398e12d19 100644 --- a/src/main/java/games/loveletter/actions/PlayCard.java +++ b/src/main/java/games/loveletter/actions/PlayCard.java @@ -5,6 +5,7 @@ import core.components.Deck; import core.components.PartialObservableDeck; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import java.util.Objects; @@ -12,15 +13,15 @@ public class PlayCard extends AbstractAction { protected final int playerID; final int targetPlayer; - protected final LoveLetterCard.CardType cardType; - final LoveLetterCard.CardType forcedCountessCardType; + protected final CardType cardType; + final CardType forcedCountessCardType; final boolean canExecuteEffect; final boolean discard; protected final int cardIdx; - LoveLetterCard.CardType targetCardType, otherCardInHand; + CardType targetCardType, otherCardInHand; - public PlayCard(LoveLetterCard.CardType cardType, int cardIdx, int playerID, int targetPlayer, LoveLetterCard.CardType targetCardType, LoveLetterCard.CardType forcedCountessCardType, boolean canExecuteEffect, boolean discard) { + public PlayCard(CardType cardType, int cardIdx, int playerID, int targetPlayer, CardType targetCardType, CardType forcedCountessCardType, boolean canExecuteEffect, boolean discard) { this.cardType = cardType; this.playerID = playerID; this.targetPlayer = targetPlayer; @@ -30,7 +31,7 @@ public PlayCard(LoveLetterCard.CardType cardType, int cardIdx, int playerID, int this.discard = discard; this.cardIdx = cardIdx; } - public PlayCard(int cardIdx, int playerID, boolean discard, LoveLetterCard.CardType targetCardType) { + public PlayCard(int cardIdx, int playerID, boolean discard, CardType targetCardType) { this.cardType = null; this.playerID = playerID; this.targetPlayer = -1; @@ -137,15 +138,15 @@ public int getTargetPlayer() { return targetPlayer; } - public LoveLetterCard.CardType getCardType() { + public CardType getCardType() { return cardType; } - public LoveLetterCard.CardType getForcedCountessCardType() { + public CardType getForcedCountessCardType() { return forcedCountessCardType; } - public LoveLetterCard.CardType getTargetCardType() { + public CardType getTargetCardType() { return targetCardType; } @@ -157,7 +158,7 @@ public boolean isDiscard() { return discard; } - public LoveLetterCard.CardType getOtherCardInHand() { + public CardType getOtherCardInHand() { return otherCardInHand; } diff --git a/src/main/java/games/loveletter/actions/PriestAction.java b/src/main/java/games/loveletter/actions/PriestAction.java index 9c9a9139c..9efc8c5c1 100644 --- a/src/main/java/games/loveletter/actions/PriestAction.java +++ b/src/main/java/games/loveletter/actions/PriestAction.java @@ -1,10 +1,10 @@ package games.loveletter.actions; import core.AbstractGameState; -import core.CoreConstants; import core.components.PartialObservableDeck; import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; /** @@ -14,7 +14,7 @@ public class PriestAction extends PlayCard implements IPrintable { public PriestAction(int cardIdx, int playerID, int opponentID, boolean canExecuteEffect, boolean discard) { - super(LoveLetterCard.CardType.Priest, cardIdx, playerID, opponentID, null, null, canExecuteEffect, discard); + super(CardType.Priest, cardIdx, playerID, opponentID, null, null, canExecuteEffect, discard); } @Override diff --git a/src/main/java/games/loveletter/actions/PrinceAction.java b/src/main/java/games/loveletter/actions/PrinceAction.java index 703fd2b48..ded638f6b 100644 --- a/src/main/java/games/loveletter/actions/PrinceAction.java +++ b/src/main/java/games/loveletter/actions/PrinceAction.java @@ -4,10 +4,9 @@ import core.components.Deck; import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; -import java.util.Objects; - /** * The targeted player discards its current and draws a new one. * In case the discarded card is a princess, the targeted player is removed from the game. @@ -15,7 +14,7 @@ public class PrinceAction extends PlayCard implements IPrintable { public PrinceAction(int cardIdx, int playerID, int opponentID, boolean canExecuteEffect, boolean discard) { - super(LoveLetterCard.CardType.Prince, cardIdx, playerID, opponentID, null, null, canExecuteEffect, discard); + super(CardType.Prince, cardIdx, playerID, opponentID, null, null, canExecuteEffect, discard); } @Override @@ -29,7 +28,7 @@ protected boolean _execute(LoveLetterGameState llgs) { // if the discarded card is a princess, the targeted player loses the game targetCardType = card.cardType; - if (targetCardType == LoveLetterCard.CardType.Princess) { + if (targetCardType == CardType.Princess) { llgs.killPlayer(playerID, targetPlayer, cardType); if (llgs.getCoreGameParameters().recordEventHistory) { llgs.recordHistory("Player " + targetPlayer + " discards Princess and loses!"); diff --git a/src/main/java/games/loveletter/actions/deep/DeepBaronAction.java b/src/main/java/games/loveletter/actions/deep/DeepBaronAction.java index 76037492c..67c9a5741 100644 --- a/src/main/java/games/loveletter/actions/deep/DeepBaronAction.java +++ b/src/main/java/games/loveletter/actions/deep/DeepBaronAction.java @@ -3,7 +3,7 @@ import core.AbstractGameState; import core.actions.AbstractAction; import games.loveletter.LoveLetterGameState; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.List; @@ -13,7 +13,7 @@ public class DeepBaronAction extends PlayCardDeep { public DeepBaronAction(int cardIdx, int playerID) { - super(LoveLetterCard.CardType.Baron, cardIdx, playerID); + super(CardType.Baron, cardIdx, playerID); } @Override @@ -24,7 +24,7 @@ public boolean equals(Object obj) { @Override public List _computeAvailableActions(AbstractGameState state) { assert cardType != null; - return cardType.getFlatActions((LoveLetterGameState) state, cardIdx, playerID, false); + return cardType.flatActions((LoveLetterGameState) state, cardIdx, playerID, false); } @Override diff --git a/src/main/java/games/loveletter/actions/deep/DeepGuardAction.java b/src/main/java/games/loveletter/actions/deep/DeepGuardAction.java index 1cf95f529..f860799c1 100644 --- a/src/main/java/games/loveletter/actions/deep/DeepGuardAction.java +++ b/src/main/java/games/loveletter/actions/deep/DeepGuardAction.java @@ -7,7 +7,7 @@ import core.interfaces.IPrintable; import games.loveletter.LoveLetterGameState; import games.loveletter.actions.PlayCard; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.ArrayList; import java.util.List; @@ -27,7 +27,7 @@ enum Step { private Step step; public DeepGuardAction(int cardIdx, int playerID) { - super(LoveLetterCard.CardType.Guard, cardIdx, playerID); + super(CardType.Guard, cardIdx, playerID); step = Step.TargetPlayer; targetPlayer = -1; } @@ -61,7 +61,7 @@ public List _computeAvailableActions(AbstractGameState state) { if (cardActions.size() == 0) cardActions.add(new ChoosePlayer(-1)); } else { // Complete actions - cardActions.addAll(LoveLetterCard.CardType.Guard.getFlatActions(gs, new PlayCard(cardIdx, playerID, false, targetPlayer))); + cardActions.addAll(CardType.Guard.flatActions(gs, new PlayCard(cardIdx, playerID, false, targetPlayer))); } return cardActions; } diff --git a/src/main/java/games/loveletter/actions/deep/DeepKingAction.java b/src/main/java/games/loveletter/actions/deep/DeepKingAction.java index 8c06c1b17..e17541d30 100644 --- a/src/main/java/games/loveletter/actions/deep/DeepKingAction.java +++ b/src/main/java/games/loveletter/actions/deep/DeepKingAction.java @@ -3,7 +3,7 @@ import core.AbstractGameState; import core.actions.AbstractAction; import games.loveletter.LoveLetterGameState; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.List; @@ -13,7 +13,7 @@ public class DeepKingAction extends PlayCardDeep { public DeepKingAction(int cardIdx, int playerID) { - super(LoveLetterCard.CardType.King, cardIdx, playerID); + super(CardType.King, cardIdx, playerID); } @Override @@ -24,7 +24,7 @@ public boolean equals(Object obj) { @Override public List _computeAvailableActions(AbstractGameState state) { assert cardType != null; - return cardType.getFlatActions((LoveLetterGameState) state, cardIdx, playerID, false); + return cardType.flatActions((LoveLetterGameState) state, cardIdx, playerID, false); } @Override diff --git a/src/main/java/games/loveletter/actions/deep/DeepPriestAction.java b/src/main/java/games/loveletter/actions/deep/DeepPriestAction.java index ed922cbf8..132513d75 100644 --- a/src/main/java/games/loveletter/actions/deep/DeepPriestAction.java +++ b/src/main/java/games/loveletter/actions/deep/DeepPriestAction.java @@ -3,7 +3,7 @@ import core.AbstractGameState; import core.actions.AbstractAction; import games.loveletter.LoveLetterGameState; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.List; @@ -14,7 +14,7 @@ public class DeepPriestAction extends PlayCardDeep { public DeepPriestAction(int cardIdx, int playerID) { - super(LoveLetterCard.CardType.Priest, cardIdx, playerID); + super(CardType.Priest, cardIdx, playerID); } @Override @@ -25,7 +25,7 @@ public boolean equals(Object obj) { @Override public List _computeAvailableActions(AbstractGameState state) { assert cardType != null; - return cardType.getFlatActions((LoveLetterGameState) state, cardIdx, playerID, false); + return cardType.flatActions((LoveLetterGameState) state, cardIdx, playerID, false); } @Override diff --git a/src/main/java/games/loveletter/actions/deep/DeepPrinceAction.java b/src/main/java/games/loveletter/actions/deep/DeepPrinceAction.java index 7d7f76334..0d355016d 100644 --- a/src/main/java/games/loveletter/actions/deep/DeepPrinceAction.java +++ b/src/main/java/games/loveletter/actions/deep/DeepPrinceAction.java @@ -3,7 +3,7 @@ import core.AbstractGameState; import core.actions.AbstractAction; import games.loveletter.LoveLetterGameState; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.List; @@ -14,13 +14,13 @@ public class DeepPrinceAction extends PlayCardDeep { public DeepPrinceAction(int cardIdx, int playerId) { - super(LoveLetterCard.CardType.Prince, cardIdx, playerId); + super(CardType.Prince, cardIdx, playerId); } @Override public List _computeAvailableActions(AbstractGameState state) { assert cardType != null; - return cardType.getFlatActions((LoveLetterGameState) state, cardIdx, playerID, false); + return cardType.flatActions((LoveLetterGameState) state, cardIdx, playerID, false); } @Override diff --git a/src/main/java/games/loveletter/actions/deep/PlayCardDeep.java b/src/main/java/games/loveletter/actions/deep/PlayCardDeep.java index 417869d3d..5de126bd2 100644 --- a/src/main/java/games/loveletter/actions/deep/PlayCardDeep.java +++ b/src/main/java/games/loveletter/actions/deep/PlayCardDeep.java @@ -4,14 +4,14 @@ import core.actions.AbstractAction; import core.interfaces.IExtendedSequence; import games.loveletter.actions.PlayCard; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.Objects; public abstract class PlayCardDeep extends PlayCard implements IExtendedSequence { private boolean executed; - public PlayCardDeep(LoveLetterCard.CardType cardType, int cardIdx, int playerID) { + public PlayCardDeep(CardType cardType, int cardIdx, int playerID) { super(cardType, cardIdx, playerID, -1, null, null, false, true); } diff --git a/src/main/java/games/loveletter/cards/CardType.java b/src/main/java/games/loveletter/cards/CardType.java new file mode 100644 index 000000000..d5c87e727 --- /dev/null +++ b/src/main/java/games/loveletter/cards/CardType.java @@ -0,0 +1,196 @@ +package games.loveletter.cards; + +import core.CoreConstants; +import core.actions.AbstractAction; +import games.loveletter.LoveLetterGameState; +import games.loveletter.LoveLetterParameters; +import games.loveletter.actions.*; +import games.loveletter.actions.deep.*; + +import java.util.*; +import java.util.function.*; + +// each card consists of a type and a value (between 1 and 8). the card type defines the actions available to the player +public enum CardType { + Princess(8, "In case the princess is discarded or played the player is immediately removed from the game."), + Countess(7, "The Countess needs to be discarded in case the player also hold a King or a Prince card."), + King(6, "The King lets two players swap their hand cards."), + Prince(5, "The targeted player discards its current and draws a new one."), + Handmaid(4, "The handmaid protects the player from any targeted effects until the next turn."), + Baron(3, "The Baron lets two players compare their hand card. The player with the lesser valued card is removed from the game."), + Priest(2, "The Priest allows a player to see another player's hand cards."), + Guard(1, "The guard allows to attempt guessing another player's card. If the guess is correct, the targeted opponent is removed from the game."); + + private final String cardText; + private final int value; + + CardType(int value, String text) { + this.value = value; + this.cardText = text; + } + + /** Numeric value of card from 1 to 8 */ + public int getValue() { + return value; + } + + // The value of the highest card in the game + public static int getMaxCardValue() { + return 8; + } + + // Summarises the functionality of the card + public String getCardText(LoveLetterParameters params) { + return this.name() + " (" + value + "; x" + params.cardCounts.get(this) + "): " + cardText; + } + + // Action factory + private BiFunction> generateFlatActions, generateDeepActions; + + static { + Princess.generateFlatActions = (gs, play) -> Collections.singletonList(new PlayCard(CardType.Princess, play.getCardIdx(), play.getPlayerID(), -1, null, null, true, true)); + Handmaid.generateFlatActions = (gs, play) -> Collections.singletonList(new HandmaidAction(play.getCardIdx(), play.getPlayerID())); + Countess.generateFlatActions = (gs, play) -> Collections.singletonList( + new PlayCard(Countess, play.getCardIdx(), play.getPlayerID(), -1, null, gs.needToForceCountess(gs.getPlayerHandCards().get(play.getPlayerID())), true, play.isDiscard())); + Priest.generateFlatActions = (gs, play) -> { + int p = play.getPlayerID(); + boolean discard = play.isDiscard(); + List cardActions = new ArrayList<>(); + for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { + if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) + continue; + cardActions.add(new PriestAction(play.getCardIdx(), p, targetPlayer, true, discard)); + } + if (cardActions.isEmpty()) cardActions.add(new PriestAction(play.getCardIdx(), p, p, false, discard)); + return cardActions; + }; + Priest.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepPriestAction(play.getCardIdx(), play.getPlayerID())); + King.generateFlatActions = (gs, play) -> { + int p = play.getPlayerID(); + boolean discard = play.isDiscard(); + List cardActions = new ArrayList<>(); + for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { + if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) + continue; + cardActions.add(new KingAction(play.getCardIdx(), p, targetPlayer, true, discard)); + } + if (cardActions.isEmpty()) cardActions.add(new KingAction(play.getCardIdx(), p, p, false, discard)); + return cardActions; + }; + King.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepKingAction(play.getCardIdx(), play.getPlayerID())); + Baron.generateFlatActions = (gs, play) -> { + int p = play.getPlayerID(); + boolean discard = play.isDiscard(); + List cardActions = new ArrayList<>(); + for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { + if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) + continue; + cardActions.add(new BaronAction(play.getCardIdx(), p, targetPlayer, true, discard)); + } + if (cardActions.isEmpty()) cardActions.add(new BaronAction(play.getCardIdx(), p, p, false, discard)); + return cardActions; + }; + Baron.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepBaronAction(play.getCardIdx(), play.getPlayerID())); + Prince.generateFlatActions = (gs, play) -> { + int p = play.getPlayerID(); + boolean discard = play.isDiscard(); + List cardActions = new ArrayList<>(); + for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { + if (gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) + continue; + cardActions.add(new PrinceAction(play.getCardIdx(), p, targetPlayer, true, discard)); + } + return cardActions; + }; + Prince.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepPrinceAction(play.getCardIdx(), play.getPlayerID())); + Guard.generateFlatActions = (gs, play) -> { + int p = play.getPlayerID(); + boolean discard = play.isDiscard(); + int target = play.getTargetPlayer(); + + List cardActions = new ArrayList<>(); + if (target == -1) { + for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { + if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) + continue; + for (CardType type : CardType.values()) { + if (type != CardType.Guard) { + cardActions.add(new GuardAction(play.getCardIdx(), p, targetPlayer, type, true, discard)); + } + } + } + } else { + for (CardType type : CardType.values()) { + if (type != CardType.Guard) { + cardActions.add(new GuardAction(play.getCardIdx(), p, target, type, true, discard)); + } + } + } + if (cardActions.isEmpty()) { + // Target yourself, but don't guess yourself out of the game + cardActions.add(new GuardAction(play.getCardIdx(), p, p, Guard, false, discard)); + } + return cardActions; + }; + Guard.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepGuardAction(play.getCardIdx(), play.getPlayerID())); + } + + public List flatActions(LoveLetterGameState gs, PlayCard play) { + if (generateFlatActions != null) return generateFlatActions.apply(gs, play); + return new ArrayList<>(); + } + + public List flatActions(LoveLetterGameState gs, int cardIdx, int playerId, boolean discard) { + if (generateFlatActions != null) return generateFlatActions.apply(gs, new PlayCard(cardIdx, playerId, discard)); + return new ArrayList<>(); + } + + public List deepActions(LoveLetterGameState gs, int cardIdx, int playerId, boolean discard) { + PlayCard play = new PlayCard(cardIdx, playerId, discard); + if (generateDeepActions != null) return generateDeepActions.apply(gs, play); + return flatActions(gs, play); + } + + // To string + private Function generateString; + + static { + Princess.generateString = play -> "Princess (" + play.getPlayerID() + " loses the game)"; + Countess.generateString = play -> { + if (play.getForcedCountessCardType() == null) return "Countess (no effect)"; + return "Countess (auto discard with " + play.getForcedCountessCardType() + ")"; + }; + King.generateString = play -> "King (" + play.getPlayerID() + " trades hands with " + play.getTargetPlayer() + ")"; + Prince.generateString = play -> "Prince (" + play.getTargetPlayer() + " discards " + (play.getTargetCardType() != null ? play.getTargetCardType() : "card") + " and draws a new card)"; + Handmaid.generateString = play -> "Handmaid (" + play.getPlayerID() + " is protected until their next turn)"; + Baron.generateString = play -> { + if (play.getTargetCardType() == null) { + return "Baron (" + play.getPlayerID() + " compares cards with " + play.getTargetPlayer() + ")"; + } else { + return "Baron (" + play.getPlayerID() + " " + play.getOtherCardInHand() + " vs " + play.getTargetPlayer() + " " + play.getTargetCardType() + ")"; + } + }; + Priest.generateString = play -> "Priest (" + play.getPlayerID() + " sees " + (play.getTargetCardType() != null ? play.getTargetCardType() : "card") + " of " + play.getTargetPlayer() + ")"; + Guard.generateString = play -> "Guard (" + play.getPlayerID() + " guess " + play.getTargetPlayer() + " holds card " + play.getTargetCardType().name() + ")"; + } + + public String getString(PlayCard play) { + if (generateString != null) { + return generateString.apply(play); + } else return ""; + } + + // Execute + private BiConsumer execute; + + public void execute(LoveLetterGameState gs, PlayCard play) { + if (execute != null) { + execute.accept(gs, play); + } + } + + static { + Princess.execute = (gs, play) -> gs.killPlayer(play.getPlayerID(), play.getPlayerID(), Princess); + + } +} diff --git a/src/main/java/games/loveletter/cards/LoveLetterCard.java b/src/main/java/games/loveletter/cards/LoveLetterCard.java index 46596d923..8d43e40fc 100644 --- a/src/main/java/games/loveletter/cards/LoveLetterCard.java +++ b/src/main/java/games/loveletter/cards/LoveLetterCard.java @@ -1,186 +1,9 @@ package games.loveletter.cards; -import core.CoreConstants; -import core.actions.AbstractAction; import core.components.Card; -import games.loveletter.LoveLetterGameState; -import games.loveletter.LoveLetterParameters; -import games.loveletter.actions.*; -import games.loveletter.actions.deep.*; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Function; public class LoveLetterCard extends Card { - // each card consists of a type and a value. the card type defines the actions available to the player - public enum CardType { - Princess(8, "In case the princess is discarded or played the player is immediately removed from the game."), - Countess(7, "The Countess needs to be discarded in case the player also hold a King or a Prince card."), - King(6, "The King lets two players swap their hand cards."), - Prince(5, "The targeted player discards its current and draws a new one."), - Handmaid(4, "The handmaid protects the player from any targeted effects until the next turn."), - Baron(3, "The Baron lets two players compare their hand card. The player with the lesser valued card is removed from the game."), - Priest(2, "The Priest allows a player to see another player's hand cards."), - Guard(1, "The guard allows to attempt guessing another player's card. If the guess is correct, the targeted opponent is removed from the game."); - - private final String cardText; - private final int value; - CardType(int value, String text){ - this.value = value; - this.cardText = text; - } - public int getValue(){ return value;} - public static int getMaxCardValue() { return 8; } - public String getCardText(LoveLetterParameters params) { - return this.name() + " (" + value + "; x" + params.cardCounts.get(this) + "): " + cardText; - } - - // Action factory - private BiFunction> generateFlatActions, generateDeepActions; - static { - Princess.generateFlatActions = (gs, play) -> Collections.singletonList(new PlayCard(LoveLetterCard.CardType.Princess, play.getCardIdx(), play.getPlayerID(), -1, null, null, true, true)); - Handmaid.generateFlatActions = (gs, play) -> Collections.singletonList(new HandmaidAction(play.getCardIdx(), play.getPlayerID())); - Countess.generateFlatActions = (gs, play) -> Collections.singletonList( - new PlayCard(Countess, play.getCardIdx(), play.getPlayerID(), -1, null, gs.needToForceCountess(gs.getPlayerHandCards().get(play.getPlayerID())), true, play.isDiscard())); - Priest.generateFlatActions = (gs, play) -> { - int p = play.getPlayerID(); - boolean discard = play.isDiscard(); - List cardActions = new ArrayList<>(); - for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { - if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) - continue; - cardActions.add(new PriestAction(play.getCardIdx(), p, targetPlayer, true, discard)); - } - if (cardActions.isEmpty()) cardActions.add(new PriestAction(play.getCardIdx(), p, p, false, discard)); - return cardActions; - }; - Priest.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepPriestAction(play.getCardIdx(), play.getPlayerID())); - King.generateFlatActions = (gs, play) -> { - int p = play.getPlayerID(); - boolean discard = play.isDiscard(); - List cardActions = new ArrayList<>(); - for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { - if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) - continue; - cardActions.add(new KingAction(play.getCardIdx(), p, targetPlayer, true, discard)); - } - if (cardActions.isEmpty()) cardActions.add(new KingAction(play.getCardIdx(), p, p, false, discard)); - return cardActions; - }; - King.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepKingAction(play.getCardIdx(), play.getPlayerID())); - Baron.generateFlatActions = (gs, play) -> { - int p = play.getPlayerID(); - boolean discard = play.isDiscard(); - List cardActions = new ArrayList<>(); - for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { - if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) - continue; - cardActions.add(new BaronAction(play.getCardIdx(), p, targetPlayer, true, discard)); - } - if (cardActions.isEmpty()) cardActions.add(new BaronAction(play.getCardIdx(), p, p, false, discard)); - return cardActions;}; - Baron.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepBaronAction(play.getCardIdx(), play.getPlayerID())); - Prince.generateFlatActions = (gs, play) -> { - int p = play.getPlayerID(); - boolean discard = play.isDiscard(); - List cardActions = new ArrayList<>(); - for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { - if (gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) - continue; - cardActions.add(new PrinceAction(play.getCardIdx(), p, targetPlayer, true, discard)); - } - return cardActions;}; - Prince.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepPrinceAction(play.getCardIdx(), play.getPlayerID())); - Guard.generateFlatActions = (gs, play) -> { - int p = play.getPlayerID(); - boolean discard = play.isDiscard(); - int target = play.getTargetPlayer(); - - List cardActions = new ArrayList<>(); - if (target == -1) { - for (int targetPlayer = 0; targetPlayer < gs.getNPlayers(); targetPlayer++) { - if (targetPlayer == p || gs.getPlayerResults()[targetPlayer] == CoreConstants.GameResult.LOSE_ROUND || gs.isProtected(targetPlayer)) - continue; - for (LoveLetterCard.CardType type : LoveLetterCard.CardType.values()) { - if (type != LoveLetterCard.CardType.Guard) { - cardActions.add(new GuardAction(play.getCardIdx(), p, targetPlayer, type, true, discard)); - } - } - } - } else { - for (LoveLetterCard.CardType type : LoveLetterCard.CardType.values()) { - if (type != LoveLetterCard.CardType.Guard) { - cardActions.add(new GuardAction(play.getCardIdx(), p, target, type, true, discard)); - } - } - } - if (cardActions.isEmpty()) { - // Target yourself, but don't guess yourself out of the game - cardActions.add(new GuardAction(play.getCardIdx(), p, p, Guard, false, discard)); - } - return cardActions; - }; - Guard.generateDeepActions = (gs, play) -> Collections.singletonList(new DeepGuardAction(play.getCardIdx(), play.getPlayerID())); - } - public List getFlatActions(LoveLetterGameState gs, PlayCard play) { - if (generateFlatActions != null) return generateFlatActions.apply(gs, play); - return new ArrayList<>(); - } - public List getFlatActions(LoveLetterGameState gs, int cardIdx, int playerId, boolean discard) { - if (generateFlatActions != null) return generateFlatActions.apply(gs, new PlayCard(cardIdx, playerId, discard)); - return new ArrayList<>(); - } - public List getDeepActions(LoveLetterGameState gs, int cardIdx, int playerId, boolean discard) { - PlayCard play = new PlayCard(cardIdx, playerId, discard); - if (generateDeepActions != null) return generateDeepActions.apply(gs, play); - return getFlatActions(gs, play); - } - - // To string - private Function generateString; - static { - Princess.generateString = play -> "Princess (" + play.getPlayerID() + " loses the game)"; - Countess.generateString = play -> { - if (play.getForcedCountessCardType() == null) return "Countess (no effect)"; - return "Countess (auto discard with " + play.getForcedCountessCardType() + ")"; - }; - King.generateString = play -> "King (" + play.getPlayerID() + " trades hands with " + play.getTargetPlayer() + ")"; - Prince.generateString = play -> "Prince (" + play.getTargetPlayer() + " discards " + (play.getTargetCardType() != null? play.getTargetCardType() : "card") + " and draws a new card)"; - Handmaid.generateString = play -> "Handmaid (" + play.getPlayerID() + " is protected until their next turn)"; - Baron.generateString = play -> { - if (play.getTargetCardType() == null) { - return "Baron (" + play.getPlayerID() + " compares cards with " + play.getTargetPlayer() + ")"; - } else { - return "Baron (" + play.getPlayerID() + " " + play.getOtherCardInHand() + " vs " + play.getTargetPlayer() + " " + play.getTargetCardType() + ")"; - } - }; - Priest.generateString = play -> "Priest (" + play.getPlayerID() + " sees " + (play.getTargetCardType() != null? play.getTargetCardType() : "card") + " of " + play.getTargetPlayer() + ")"; - Guard.generateString = play -> "Guard (" + play.getPlayerID() + " guess " + play.getTargetPlayer() + " holds card " + play.getTargetCardType().name() + ")"; - } - public String getString(PlayCard play) { - if (generateString != null) { - return generateString.apply(play); - } else return ""; - } - - // Execute - private BiConsumer execute; - public void execute(LoveLetterGameState gs, PlayCard play) { - if (execute != null) { - execute.accept(gs, play); - } - } - static { - Princess.execute = (gs, play) -> gs.killPlayer(play.getPlayerID(), play.getPlayerID(), Princess); - - } - } - public final CardType cardType; public LoveLetterCard(CardType cardType) { diff --git a/src/main/java/games/loveletter/features/LLActionFeaturesLarge.java b/src/main/java/games/loveletter/features/LLActionFeaturesLarge.java index 01c196836..912b8e9d6 100644 --- a/src/main/java/games/loveletter/features/LLActionFeaturesLarge.java +++ b/src/main/java/games/loveletter/features/LLActionFeaturesLarge.java @@ -6,12 +6,12 @@ import core.interfaces.IActionFeatureVector; import games.loveletter.LoveLetterGameState; import games.loveletter.actions.*; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import java.util.ArrayList; import java.util.List; -import static java.util.stream.Collectors.toList; import static utilities.Utils.enumNames; import static utilities.Utils.enumToOneHot; @@ -19,16 +19,16 @@ public class LLActionFeaturesLarge implements IActionFeatureVector { final String[] localNames; - final int featuresPerGroup = LoveLetterCard.CardType.values().length; + final int featuresPerGroup = CardType.values().length; public LLActionFeaturesLarge() { List allNames = new ArrayList<>(); // For each card type, is this the card played - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_PLAY").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_PLAY").toList()); // For each card type, is this the card guessed (where that is relevant - for GUARD) - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_GUESS").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_GUESS").toList()); // For each card type, does the target player have this card (to our knowledge) - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_HAS").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_HAS").toList()); // current position of target player allNames.add("TARGET_ORDINAL"); localNames = allNames.toArray(new String[0]); @@ -45,8 +45,8 @@ public double[] featureVector(AbstractAction a, AbstractGameState state, int pla LoveLetterGameState llgs = (LoveLetterGameState) state; if (!(a instanceof PlayCard)) return retValue; - LoveLetterCard.CardType cardPlayed = ((PlayCard) a).getCardType(); - LoveLetterCard.CardType cardGuessed = ((PlayCard) a).getTargetCardType(); + CardType cardPlayed = ((PlayCard) a).getCardType(); + CardType cardGuessed = ((PlayCard) a).getTargetCardType(); System.arraycopy(enumToOneHot(cardPlayed), 0, retValue, 0, featuresPerGroup); if (cardGuessed != null) System.arraycopy(enumToOneHot(cardGuessed), 0, retValue, featuresPerGroup, featuresPerGroup); diff --git a/src/main/java/games/loveletter/features/LLActionFeaturesMedium.java b/src/main/java/games/loveletter/features/LLActionFeaturesMedium.java index 289604fb6..3ca2e8536 100644 --- a/src/main/java/games/loveletter/features/LLActionFeaturesMedium.java +++ b/src/main/java/games/loveletter/features/LLActionFeaturesMedium.java @@ -5,7 +5,7 @@ import core.interfaces.IActionFeatureVector; import games.loveletter.LoveLetterGameState; import games.loveletter.actions.PlayCard; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; import java.util.ArrayList; import java.util.List; @@ -17,14 +17,14 @@ public class LLActionFeaturesMedium implements IActionFeatureVector { final String[] localNames; - final int featuresPerGroup = LoveLetterCard.CardType.values().length; + final int featuresPerGroup = CardType.values().length; public LLActionFeaturesMedium() { List allNames = new ArrayList<>(); // For each card type, is this the card played - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_PLAY").collect(toList())); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_PLAY").collect(toList())); // For each card type, is this the card guessed (where that is relevant - for GUARD) - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_GUESS").collect(toList())); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_GUESS").collect(toList())); localNames = allNames.toArray(new String[0]); } @@ -39,8 +39,8 @@ public double[] featureVector(AbstractAction a, AbstractGameState state, int pla LoveLetterGameState llgs = (LoveLetterGameState) state; if (!(a instanceof PlayCard)) return retValue; - LoveLetterCard.CardType cardPlayed = ((PlayCard) a).getCardType(); - LoveLetterCard.CardType cardGuessed = ((PlayCard) a).getTargetCardType(); + CardType cardPlayed = ((PlayCard) a).getCardType(); + CardType cardGuessed = ((PlayCard) a).getTargetCardType(); System.arraycopy(enumToOneHot(cardPlayed), 0, retValue, 0, featuresPerGroup); if (cardGuessed != null) System.arraycopy(enumToOneHot(cardGuessed), 0, retValue, featuresPerGroup, featuresPerGroup); diff --git a/src/main/java/games/loveletter/features/LLActionFeaturesTiny.java b/src/main/java/games/loveletter/features/LLActionFeaturesTiny.java index 0b2e53f8c..c64211d0d 100644 --- a/src/main/java/games/loveletter/features/LLActionFeaturesTiny.java +++ b/src/main/java/games/loveletter/features/LLActionFeaturesTiny.java @@ -4,23 +4,19 @@ import core.actions.AbstractAction; import core.interfaces.IActionFeatureVector; import games.loveletter.actions.PlayCard; -import games.loveletter.cards.LoveLetterCard; +import games.loveletter.cards.CardType; -import java.util.ArrayList; -import java.util.List; - -import static java.util.stream.Collectors.toList; import static utilities.Utils.enumNames; import static utilities.Utils.enumToOneHot; public class LLActionFeaturesTiny implements IActionFeatureVector { final String[] localNames; - final int featuresPerGroup = LoveLetterCard.CardType.values().length; + final int featuresPerGroup = CardType.values().length; public LLActionFeaturesTiny() { // For each card type, is this the card played - localNames = enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_PLAY").toArray(String[]::new); + localNames = enumNames(CardType.class).stream().map(s -> s + "_PLAY").toArray(String[]::new); } @Override @@ -33,7 +29,7 @@ public double[] featureVector(AbstractAction a, AbstractGameState state, int pla double[] retValue = new double[names().length]; if (!(a instanceof PlayCard)) return retValue; - LoveLetterCard.CardType cardPlayed = ((PlayCard) a).getCardType(); + CardType cardPlayed = ((PlayCard) a).getCardType(); System.arraycopy(enumToOneHot(cardPlayed), 0, retValue, 0, featuresPerGroup); return retValue; } diff --git a/src/main/java/games/loveletter/features/LLStateFeatures.java b/src/main/java/games/loveletter/features/LLStateFeatures.java index 6ecc83fc3..01ff9d678 100644 --- a/src/main/java/games/loveletter/features/LLStateFeatures.java +++ b/src/main/java/games/loveletter/features/LLStateFeatures.java @@ -4,6 +4,7 @@ import core.components.PartialObservableDeck; import core.interfaces.IComponentContainer; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import players.heuristics.AbstractStateFeature; @@ -12,9 +13,6 @@ import java.util.List; import java.util.stream.IntStream; -import static games.loveletter.cards.LoveLetterCard.CardType.Guard; -import static games.loveletter.cards.LoveLetterCard.CardType.getMaxCardValue; -import static java.util.stream.Collectors.toList; import static utilities.Utils.enumNames; public class LLStateFeatures extends AbstractStateFeature { @@ -25,13 +23,13 @@ public class LLStateFeatures extends AbstractStateFeature { public LLStateFeatures() { List allNames = new ArrayList<>(Arrays.asList("PROTECTED", "HIDDEN", "CARDS", "DRAW_DECK")); // For each card type, do we have one in hand - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_HAND").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_HAND").toList()); // Card is in Hand and is also Known to at least one other player - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_KNOWN").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_KNOWN").toList()); // Percentage of card type in Discard - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_DISCARD").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_DISCARD").toList()); // Card is known to be in hand of another player - allNames.addAll(enumNames(LoveLetterCard.CardType.class).stream().map(s -> s + "_OTHER").toList()); + allNames.addAll(enumNames(CardType.class).stream().map(s -> s + "_OTHER").toList()); localNames = allNames.toArray(new String[0]); } @@ -55,7 +53,7 @@ protected double[] localFeatureVector(AbstractGameState gs, int playerID) { LoveLetterGameState state = (LoveLetterGameState) gs; double[] retValue = new double[localNames.length]; - int featuresPerGroup = LoveLetterCard.CardType.values().length; + int featuresPerGroup = CardType.values().length; double cardValues = 0; PartialObservableDeck hand = state.getPlayerHandCards().get(playerID); diff --git a/src/main/java/games/loveletter/features/LLStateFeaturesReduced.java b/src/main/java/games/loveletter/features/LLStateFeaturesReduced.java index 579792fbf..512e4ea91 100644 --- a/src/main/java/games/loveletter/features/LLStateFeaturesReduced.java +++ b/src/main/java/games/loveletter/features/LLStateFeaturesReduced.java @@ -5,13 +5,14 @@ import core.interfaces.IStateFeatureVector; import games.loveletter.LoveLetterGameState; import games.loveletter.LoveLetterParameters; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import java.util.HashSet; import java.util.Set; import java.util.stream.IntStream; -import static games.loveletter.cards.LoveLetterCard.CardType.*; +import static games.loveletter.cards.CardType.*; /** * A set of features designed to tie in exactly with those used in LoveLetterHeuristic @@ -31,7 +32,7 @@ public double[] featureVector(AbstractGameState gs, int playerId) { double cardValues = 0; - Set cardTypes = new HashSet<>(); + Set cardTypes = new HashSet<>(); for (LoveLetterCard card : llgs.getPlayerHandCards().get(playerId).getComponents()) { cardValues += card.cardType.getValue(); cardTypes.add(card.cardType); diff --git a/src/main/java/games/loveletter/features/LLStateFeaturesTunable.java b/src/main/java/games/loveletter/features/LLStateFeaturesTunable.java index 71cda8c49..3d639dcfc 100644 --- a/src/main/java/games/loveletter/features/LLStateFeaturesTunable.java +++ b/src/main/java/games/loveletter/features/LLStateFeaturesTunable.java @@ -4,6 +4,7 @@ import core.components.PartialObservableDeck; import evaluation.features.TunableStateFeatures; import games.loveletter.LoveLetterGameState; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import java.util.HashSet; @@ -11,7 +12,7 @@ import java.util.stream.IntStream; import static core.CoreConstants.GameResult.LOSE_ROUND; -import static games.loveletter.cards.LoveLetterCard.CardType.*; +import static games.loveletter.cards.CardType.*; /** * A set of features designed to tie in exactly with those used in LoveLetterHeuristic @@ -37,7 +38,7 @@ public double[] fullFeatureVector(AbstractGameState gs, int playerId) { double cardValues = 0; - Set cardTypes = new HashSet<>(); + Set cardTypes = new HashSet<>(); if (active[0]) { for (LoveLetterCard card : llgs.getPlayerHandCards().get(playerId).getComponents()) { cardValues += card.cardType.getValue(); diff --git a/src/main/java/games/loveletter/gui/LoveLetterGUIManager.java b/src/main/java/games/loveletter/gui/LoveLetterGUIManager.java index 5b9d32e55..3f60b6c3b 100644 --- a/src/main/java/games/loveletter/gui/LoveLetterGUIManager.java +++ b/src/main/java/games/loveletter/gui/LoveLetterGUIManager.java @@ -321,7 +321,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { llgs = (LoveLetterGameState)gameState.copy(); for (int i = 0; i < gameState.getNPlayers(); i++) { boolean front = i == gameState.getCurrentPlayer() && gameState.getCoreGameParameters().alwaysDisplayCurrentPlayer - || humanPlayerId.contains(i) + || humanPlayerIds.contains(i) || gameState.getCoreGameParameters().alwaysDisplayFullObservable; playerHands[i].update(llgs, front); diff --git a/src/main/java/games/loveletter/stats/LoveLetterMetrics.java b/src/main/java/games/loveletter/stats/LoveLetterMetrics.java index 07292a097..49a97be6e 100644 --- a/src/main/java/games/loveletter/stats/LoveLetterMetrics.java +++ b/src/main/java/games/loveletter/stats/LoveLetterMetrics.java @@ -12,6 +12,7 @@ import games.loveletter.LoveLetterGameState; import games.loveletter.actions.PlayCard; import games.loveletter.actions.PrinceAction; +import games.loveletter.cards.CardType; import games.loveletter.cards.LoveLetterCard; import java.util.*; @@ -41,12 +42,12 @@ protected boolean _run(MetricsGameListener listener, Event e, Map getDefaultEventTypes() { public static class CardStatsGameEvent extends AbstractMetric { Set playerNames; - LoveLetterCard.CardType cardPlayed = null; + CardType cardPlayed = null; boolean successfulPlay = false; @Override @@ -117,15 +118,15 @@ protected boolean _run(MetricsGameListener listener, Event e, Map getMoneyPots() { return moneyPots; } + /** + * The currently visible community cards + **/ public Deck getCommunityCards() { return communityCards; } @@ -155,22 +161,37 @@ public List> getPlayerDecks() { return playerDecks; } + /** + * A boolean array in player order, true if player needs to call (can't just check) + **/ public boolean[] getPlayerNeedsToCall() { return playerNeedsToCall; } + /** + * A boolean array in player order, true if player is 'All In' + **/ public boolean[] getPlayerAllIn() { return playerAllIn; } + /** + * A boolean array in player order, true if player has folded + **/ public boolean[] getPlayerFold() { return playerFold; } + /** + * An array in player order. Use Counter.getValue() to get the current money of the player + **/ public Counter[] getPlayerMoney() { return playerMoney; } + /** + * An array in player order. Use Counter.getValue() to get the current bet of the player + **/ public Counter[] getPlayerBet() { return playerBet; } @@ -183,6 +204,9 @@ public void setBet(boolean bet) { this.bet = bet; } + /** + * The player id of the Big Blind + **/ public int getBigId() { return bigId; } @@ -196,7 +220,7 @@ public int getSmallId() { return getNextNonBankruptPlayer(getBigId(), -1); } - public boolean playersLeftToAct() { + public boolean isPlayerStillToAct() { for (int p = 0; p < getNPlayers(); p++) { if (playerFold[p] || playerAllIn[p] || getPlayerResults()[p] == LOSE_GAME) continue; @@ -206,7 +230,7 @@ public boolean playersLeftToAct() { return false; } - public boolean checkRoundOver() { + public boolean isRoundOver() { int stillAlive = 0; for (int i = 0; i < getNPlayers(); i++) { if (getPlayerResults()[i] != LOSE_GAME && !playerFold[i] && !playerAllIn[i]) { @@ -240,7 +264,7 @@ public int getNextNonBankruptPlayer(int fromPlayer, int direction) { return next; } - public void otherPlayerMustCall(int playerId) { + public void getPlayerMustCall(int playerId) { // Others can't check for (int i = 0; i < getNPlayers(); i++) { if (i != playerId && !playerFold[i] && !playerAllIn[i] && getPlayerResults()[i] != LOSE_GAME) { diff --git a/src/main/java/games/poker/actions/AllIn.java b/src/main/java/games/poker/actions/AllIn.java index 55be2cc21..a8e2ce8f8 100644 --- a/src/main/java/games/poker/actions/AllIn.java +++ b/src/main/java/games/poker/actions/AllIn.java @@ -5,8 +5,6 @@ import core.interfaces.IPrintable; import games.poker.PokerGameState; -import java.util.Objects; - public class AllIn extends AbstractAction implements IPrintable { private final int playerId; public AllIn(int id) { @@ -22,7 +20,7 @@ public boolean execute(AbstractGameState gameState) { pgs.getPlayerAllIn()[playerId] = true; // Others can't check, unless all in - pgs.otherPlayerMustCall(playerId); + pgs.getPlayerMustCall(playerId); return true; } diff --git a/src/main/java/games/poker/actions/Bet.java b/src/main/java/games/poker/actions/Bet.java index b611301ad..203273785 100644 --- a/src/main/java/games/poker/actions/Bet.java +++ b/src/main/java/games/poker/actions/Bet.java @@ -24,7 +24,7 @@ public boolean execute(AbstractGameState gameState) { pgs.getPlayerNeedsToCall()[playerId] = false; // Others can't check - pgs.otherPlayerMustCall(playerId); + pgs.getPlayerMustCall(playerId); return true; } diff --git a/src/main/java/games/poker/actions/Raise.java b/src/main/java/games/poker/actions/Raise.java index 050ec8384..202c74b84 100644 --- a/src/main/java/games/poker/actions/Raise.java +++ b/src/main/java/games/poker/actions/Raise.java @@ -31,7 +31,7 @@ public boolean execute(AbstractGameState gameState) { pgs.getPlayerNeedsToCall()[playerId] = false; // Others can't check - pgs.otherPlayerMustCall(playerId); + pgs.getPlayerMustCall(playerId); return true; } diff --git a/src/main/java/games/poker/gui/PokerGUIManager.java b/src/main/java/games/poker/gui/PokerGUIManager.java index 1512f35b8..d64a0fd69 100644 --- a/src/main/java/games/poker/gui/PokerGUIManager.java +++ b/src/main/java/games/poker/gui/PokerGUIManager.java @@ -322,7 +322,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { for (int i = 0; i < gameState.getNPlayers(); i++) { playerHands[i].update(pgs); if (i == gameState.getCurrentPlayer() && coreParameters.alwaysDisplayCurrentPlayer - || humanPlayerId.contains(i) + || humanPlayerIds.contains(i) || coreParameters.alwaysDisplayFullObservable) { playerHands[i].setFront(true); playerHands[i].setFocusable(true); diff --git a/src/main/java/games/puertorico/PuertoRicoGameState.java b/src/main/java/games/puertorico/PuertoRicoGameState.java index 8ff6d21b9..5efcdb96a 100644 --- a/src/main/java/games/puertorico/PuertoRicoGameState.java +++ b/src/main/java/games/puertorico/PuertoRicoGameState.java @@ -61,6 +61,7 @@ public void setCurrentRole(Role role) { this.roleOwner = getCurrentPlayer(); } + // First player (the one who chose the role) public int getRoleOwner() { return roleOwner; } @@ -197,10 +198,13 @@ public int getBuildingsOfType(PuertoRicoConstants.BuildingType type) { public boolean hasActiveBuilding(int playerID, PuertoRicoConstants.BuildingType type) { return playerBoards.get(playerID).buildings.stream().anyMatch(b -> b.buildingType == type && b.getOccupation() > 0 && !b.hasBeenUsed()); } + + // The stores of the crop type that the playerId has public int getStoresOf(int playerId, Crop crop) { return playerBoards.get(playerId).getStoresOf(crop); } + // the amount of money that the specified player has public int getDoubloons(int playerId) { return playerBoards.get(playerId).doubloons; } @@ -209,6 +213,7 @@ public void changeDoubloons(int playerId, int amount) { playerBoards.get(playerId).changeDoubloons(amount); } + // Amount of crop that is available in the general supply public int getSupplyOf(Crop crop) { return cropSupply.get(crop); } @@ -239,6 +244,7 @@ public void changeColonistsOnShip(int amount) { colonistsOnShip += amount; } + // Which crops have been sold in the market, and therefore cannot be sold again public List getMarket() { return new ArrayList<>(soldInMarket); } @@ -266,6 +272,7 @@ public void setGameEndTriggered() { gameEndTriggered = true; } + // Has a game end condition been triggered public boolean isLastRound() { return gameEndTriggered; } diff --git a/src/main/java/games/puertorico/gui/GSBuildings.java b/src/main/java/games/puertorico/gui/GSBuildings.java index 1bc40536e..7c58ef387 100644 --- a/src/main/java/games/puertorico/gui/GSBuildings.java +++ b/src/main/java/games/puertorico/gui/GSBuildings.java @@ -40,7 +40,7 @@ public void mouseClicked(MouseEvent e) { break; } } - if (clicked != null && gs.getCurrentRole() == PuertoRicoConstants.Role.BUILDER && gui.getHumanPlayerId().contains(gs.getCurrentPlayer())) { + if (clicked != null && gs.getCurrentRole() == PuertoRicoConstants.Role.BUILDER && gui.getHumanPlayerIds().contains(gs.getCurrentPlayer())) { // Clicked on a building when it's our choice of building, check if the clicked building is a legal action PuertoRicoParameters params = (PuertoRicoParameters) gs.getGameParameters(); @@ -118,7 +118,7 @@ protected void paintComponent(Graphics gg) { buildingRectMap.put(new Rectangle(x, y, buildingWidth, buildingHeight * size), b); // Highlight buildings that can be built by current player - if (gs.getCurrentRole() == PuertoRicoConstants.Role.BUILDER && gui.getHumanPlayerId().contains(gs.getCurrentPlayer())) { + if (gs.getCurrentRole() == PuertoRicoConstants.Role.BUILDER && gui.getHumanPlayerIds().contains(gs.getCurrentPlayer())) { Stroke s = g.getStroke(); g.setStroke(new BasicStroke(3)); if (b != null) { diff --git a/src/main/java/games/puertorico/gui/GSInfo.java b/src/main/java/games/puertorico/gui/GSInfo.java index db09593ad..0c391eb04 100644 --- a/src/main/java/games/puertorico/gui/GSInfo.java +++ b/src/main/java/games/puertorico/gui/GSInfo.java @@ -45,7 +45,7 @@ public GSInfo(PuertoRicoGUI gui, PuertoRicoGameState gs) { @Override public void mouseClicked(MouseEvent e) { if (gs.getCurrentRole() == PuertoRicoConstants.Role.SETTLER - && gui.getHumanPlayerId().contains(gs.getCurrentPlayer()) + && gui.getHumanPlayerIds().contains(gs.getCurrentPlayer()) && gs.getQuarriesLeft() > 0 && (gs.getCurrentPlayer() == gs.getRoleOwner() || gs.hasActiveBuilding(gs.getCurrentPlayer(), CONSTRUCTION_HUT))) { gui.getAC().addAction(new BuildQuarry()); } @@ -74,7 +74,7 @@ protected void paintComponent(Graphics gg) { if (crop == PuertoRicoConstants.Crop.QUARRY) { PRGUIUtils.drawHexagon(g, x, pad + barrelHeight/2 - barrelWidth/2, PRGUIUtils.cropColorMap.get(crop), barrelWidth, barrelWidth); if (gs.getCurrentRole() == PuertoRicoConstants.Role.SETTLER - && gui.getHumanPlayerId().contains(gs.getCurrentPlayer()) + && gui.getHumanPlayerIds().contains(gs.getCurrentPlayer()) && gs.getQuarriesLeft() > 0 && (gs.getCurrentPlayer() == gs.getRoleOwner() || gs.hasActiveBuilding(gs.getCurrentPlayer(), CONSTRUCTION_HUT))) { g.setColor(highlightColor); Stroke s = g.getStroke(); diff --git a/src/main/java/games/puertorico/gui/GSPlantations.java b/src/main/java/games/puertorico/gui/GSPlantations.java index b5ddc62e1..d3c8d1c9b 100644 --- a/src/main/java/games/puertorico/gui/GSPlantations.java +++ b/src/main/java/games/puertorico/gui/GSPlantations.java @@ -33,7 +33,7 @@ public void mouseClicked(MouseEvent e) { for (Map.Entry entry: rectToPlantationMap.entrySet()) { if (entry.getKey().contains(e.getPoint())) { if (gs.getCurrentRole() == PuertoRicoConstants.Role.SETTLER - && gui.getHumanPlayerId().contains(gs.getCurrentPlayer()) + && gui.getHumanPlayerIds().contains(gs.getCurrentPlayer()) && gs.getPlayerBoard(gs.getCurrentPlayer()).getPlantations().size() < ((PuertoRicoParameters)gs.getGameParameters()).plantationSlotsOnBoard) { if (entry.getValue() != null) { gui.getAC().addAction(new DrawPlantation(entry.getValue().crop)); @@ -53,7 +53,7 @@ public void mouseClicked(MouseEvent e) { @Override protected void paintComponent(Graphics gg) { rectToPlantationMap.clear(); - boolean highlight = gs.getCurrentRole() == PuertoRicoConstants.Role.SETTLER && gui.getHumanPlayerId().contains(gs.getCurrentPlayer()) && gs.getPlayerBoard(gs.getCurrentPlayer()).getPlantations().size() < ((PuertoRicoParameters)gs.getGameParameters()).plantationSlotsOnBoard; + boolean highlight = gs.getCurrentRole() == PuertoRicoConstants.Role.SETTLER && gui.getHumanPlayerIds().contains(gs.getCurrentPlayer()) && gs.getPlayerBoard(gs.getCurrentPlayer()).getPlantations().size() < ((PuertoRicoParameters)gs.getGameParameters()).plantationSlotsOnBoard; // Visible plantations + how many left in deck + how many discount things left Graphics2D g = (Graphics2D) gg; diff --git a/src/main/java/games/puertorico/gui/GSShipsAndMarket.java b/src/main/java/games/puertorico/gui/GSShipsAndMarket.java index 22cc9192e..39de8ba9e 100644 --- a/src/main/java/games/puertorico/gui/GSShipsAndMarket.java +++ b/src/main/java/games/puertorico/gui/GSShipsAndMarket.java @@ -100,7 +100,7 @@ protected void paintComponent(Graphics g) { Pair shipSize = drawShip((Graphics2D) g, ship, startX, pad); shipToRectMap.put(new Rectangle(startX, pad, shipSize.a, shipSize.b), s); - if (gs.getCurrentRole() == PuertoRicoConstants.Role.CAPTAIN && gui.getHumanPlayerId().contains(currentPlayer)) { + if (gs.getCurrentRole() == PuertoRicoConstants.Role.CAPTAIN && gui.getHumanPlayerIds().contains(currentPlayer)) { PuertoRicoConstants.Crop selectedCrop = gui.playerBoards[currentPlayer].cropClicked; boolean playerHasMatchingCargo = false; boolean playerHasUniqueCargo = false; diff --git a/src/main/java/games/puertorico/gui/PlayerBoard.java b/src/main/java/games/puertorico/gui/PlayerBoard.java index d83992735..658803323 100644 --- a/src/main/java/games/puertorico/gui/PlayerBoard.java +++ b/src/main/java/games/puertorico/gui/PlayerBoard.java @@ -63,7 +63,7 @@ public void mouseClicked(MouseEvent e) { plantationClicked = null; cropClicked = null; int currentPlayer = gs.getCurrentPlayer(); - if (currentPlayer != playerId && !gui.getHumanPlayerId().contains(playerId)) return; + if (currentPlayer != playerId && !gui.getHumanPlayerIds().contains(playerId)) return; PuertoRicoConstants.Role currentRole = gs.getCurrentRole(); for (Rectangle r : buildingRectMap.keySet()) { @@ -152,12 +152,12 @@ protected void paintComponent(Graphics gg) { Graphics2D g = (Graphics2D) gg; - boolean highlightCropStore = playerId == gs.getCurrentPlayer() && gui.getHumanPlayerId().contains(playerId) && + boolean highlightCropStore = playerId == gs.getCurrentPlayer() && gui.getHumanPlayerIds().contains(playerId) && (gs.getCurrentRole() == PuertoRicoConstants.Role.TRADER || gs.getCurrentRole() == PuertoRicoConstants.Role.CAPTAIN || gs.getCurrentRole() == PuertoRicoConstants.Role.DISCARD || gs.getCurrentRole() == PuertoRicoConstants.Role.CRAFTSMAN); - boolean highlightPlantationOrBuilding = playerId == gs.getCurrentPlayer() && gui.getHumanPlayerId().contains(playerId) && + boolean highlightPlantationOrBuilding = playerId == gs.getCurrentPlayer() && gui.getHumanPlayerIds().contains(playerId) && gs.getCurrentRole() == PuertoRicoConstants.Role.MAYOR; int width = size.width-borderPadX*2; diff --git a/src/main/java/games/puertorico/gui/RolePanel.java b/src/main/java/games/puertorico/gui/RolePanel.java index 18fd1ad47..96612654d 100644 --- a/src/main/java/games/puertorico/gui/RolePanel.java +++ b/src/main/java/games/puertorico/gui/RolePanel.java @@ -44,7 +44,7 @@ public void mouseMoved(MouseEvent e) { @Override public void mouseClicked(MouseEvent e) { PuertoRicoConstants.Role activeRole = gs.getCurrentRole(); - if (activeRole != null && !gui.getHumanPlayerId().contains(gs.getCurrentPlayer())) return; + if (activeRole != null && !gui.getHumanPlayerIds().contains(gs.getCurrentPlayer())) return; for (Map.Entry entry: rectangleRoleMap.entrySet()) { if (entry.getKey().contains(e.getPoint()) && gs.isRoleAvailable(entry.getValue())) { diff --git a/src/main/java/games/resistance/ResForwardModel.java b/src/main/java/games/resistance/ResForwardModel.java index 8b0a59b99..8db21c071 100644 --- a/src/main/java/games/resistance/ResForwardModel.java +++ b/src/main/java/games/resistance/ResForwardModel.java @@ -256,10 +256,8 @@ void revealCards(ResGameState resgs) { resgs.historicTeams.add(new ArrayList<>(resgs.finalTeamChoice)); resgs.noVotesPerMission.add(occurrenceCount); } - } - @Override protected void endGame(AbstractGameState gs) { gs.setGameStatus(CoreConstants.GameResult.GAME_END); @@ -272,7 +270,6 @@ public void changeLeader(ResGameState resgs) { resgs.leaderID = (resgs.leaderID + 1) % resgs.getNPlayers(); } - public static List randomiseSpies(int spies, ResGameState state, int playerID, Random rnd) { // We want to randomly assign the number of spies across the total number of players // and return a boolean[] with length of total, and spies number of true values diff --git a/src/main/java/games/resistance/ResGameState.java b/src/main/java/games/resistance/ResGameState.java index 6dbdb7f7f..b6a1602b6 100644 --- a/src/main/java/games/resistance/ResGameState.java +++ b/src/main/java/games/resistance/ResGameState.java @@ -183,6 +183,9 @@ public void addTeamChoice(ResTeamBuilding ResTeamBuilding) { teamChoice = ResTeamBuilding.getTeam(); } + /** + * Returns 0 if the player is a resistance member, 1 if the player is a spy. + */ @Override public int getTeam(int player) { ResPlayerCards.CardType id = playerHandCards.get(player).get(2).cardType; @@ -194,15 +197,30 @@ public void clearTeamChoices() { finalTeamChoice.clear(); } + /** + * The number of missions already played. + */ public int getMissionsSoFar() { return historicTeams.size(); } + + /** + * Returns the playerIDs of the team that went on the ith mission. + */ public List getHistoricTeam(int i) { return new ArrayList<>(historicTeams.get(i-1)); } + + /** + * Returns the result of the ith mission; true if successful, false if failed. + */ public boolean getHistoricMissionSuccess(int i) { return gameBoardValues.get(i-1); } + + /** + * Returns the number of failed votes on the ith mission. + */ public int getHistoricNoVotes(int i) { return noVotesPerMission.get(i-1); } @@ -241,14 +259,17 @@ public List> getPlayerHandCards() { return playerHandCards; } + // current leader who is selecting the team public int getLeaderID() { return leaderID; } + // The final team selected for the mission public List getFinalTeam() { return finalTeamChoice; } + // The list of public List getGameBoardValues() { return gameBoardValues; } diff --git a/src/main/java/games/resistance/gui/ResGUIManager.java b/src/main/java/games/resistance/gui/ResGUIManager.java index 9b96a5376..4e77ea49e 100644 --- a/src/main/java/games/resistance/gui/ResGUIManager.java +++ b/src/main/java/games/resistance/gui/ResGUIManager.java @@ -162,7 +162,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { //playerHands[i].setFocusable(true); } else { if (i == gameState.getCurrentPlayer() - || humanPlayerId.contains(i)) { + || humanPlayerIds.contains(i)) { playerHands[i].playerHandView.setFront(true); playerHands[i].setFocusable(true); } else { @@ -207,7 +207,7 @@ protected JPanel createGameStateInfoPanel(String gameTitle, AbstractGameState ga wrapper.setLayout(new FlowLayout()); wrapper.add(gameInfo); - createActionHistoryPanel(width / 2 - 10, height, humanPlayerId); + createActionHistoryPanel(width / 2 - 10, height, humanPlayerIds); wrapper.add(historyContainer); // historyInfo.setPreferredSize(new Dimension(width / 2 - 10, height)); diff --git a/src/main/java/games/stratego/StrategoGameState.java b/src/main/java/games/stratego/StrategoGameState.java index 6de09a24a..34c498541 100644 --- a/src/main/java/games/stratego/StrategoGameState.java +++ b/src/main/java/games/stratego/StrategoGameState.java @@ -74,6 +74,9 @@ protected double _getHeuristicScore(int playerId) { return new StrategoHeuristic().evaluateState(this, playerId); } + /** + * GameScore has no meaning in Stratego. This will always return zero for any non-terminal game state. + */ @Override public double getGameScore(int playerId) { return playerResults[playerId].value; diff --git a/src/main/java/games/stratego/StrategoStateFeatures.java b/src/main/java/games/stratego/StrategoStateFeatures.java index be412354f..6eaec7ef0 100644 --- a/src/main/java/games/stratego/StrategoStateFeatures.java +++ b/src/main/java/games/stratego/StrategoStateFeatures.java @@ -1,18 +1,8 @@ package games.stratego; import core.AbstractGameState; -import core.components.PartialObservableDeck; -import core.interfaces.IComponentContainer; -import games.loveletter.LoveLetterGameState; -import games.loveletter.cards.LoveLetterCard; import players.heuristics.AbstractStateFeature; -import java.util.List; -import java.util.stream.IntStream; - -import static games.loveletter.cards.LoveLetterCard.CardType.getMaxCardValue; -import static java.util.stream.Collectors.toList; - public class StrategoStateFeatures extends AbstractStateFeature { String[] localNames = new String[0]; diff --git a/src/main/java/games/sushigo/SGGameState.java b/src/main/java/games/sushigo/SGGameState.java index 80a6c8bc1..88a719760 100644 --- a/src/main/java/games/sushigo/SGGameState.java +++ b/src/main/java/games/sushigo/SGGameState.java @@ -3,13 +3,9 @@ import core.AbstractGameState; import core.AbstractParameters; import core.components.*; -import core.interfaces.IStateFeatureJSON; import games.GameType; import games.sushigo.actions.ChooseCard; import games.sushigo.cards.SGCard; -import games.wonders7.Wonders7GameParameters; -import org.json.simple.JSONObject; -import utilities.Pair; import java.util.*; @@ -110,7 +106,7 @@ protected SGGameState _copy(int playerId) { // Add player hands unseen back to the draw pile for (int p = 0; p < copy.playerHands.size(); p++) { - if (!hasSeenHand(playerId, p)) { + if (!isHandKnown(playerId, p)) { copy.drawPile.add(playerHands.get(p)); } } @@ -118,7 +114,7 @@ protected SGGameState _copy(int playerId) { // Now we draw into the unknown player hands for (int p = 0; p < copy.playerHands.size(); p++) { - if (!hasSeenHand(playerId, p)) { + if (!isHandKnown(playerId, p)) { Deck hand = copy.playerHands.get(p); int handSize = hand.getSize(); hand.clear(); @@ -144,14 +140,10 @@ protected SGGameState _copy(int playerId) { } /** - * we do know the contents of the hands of players up to T to our left, where T is the number of player turns - * so far, as we saw that hand on its way through our own - * - * @param playerId - id of player whose vision we're checking - * @param opponentId - id of opponent owning the hand of cards we're checking vision of - * @return - true if player has not seen the opponent's hand of cards, false otherwise + * we know the contents of the hands of the players that are deckRotations spaces to the left of the current player + * if this returns true, then the information provided by the playerHands is reliably correct (if false, then this information is shuffled) */ - public boolean hasSeenHand(int playerId, int opponentId) { + public boolean isHandKnown(int playerId, int opponentId) { // Player 0 is one space to the 'Left' of player 1 int opponentSpacesToLeft = (playerId - opponentId + getNPlayers()) % getNPlayers(); return opponentSpacesToLeft <= deckRotations; @@ -166,6 +158,9 @@ public void addPlayerScore(int p, int amount, SGCard.SGCardType fromType) { pointsPerCardType[p].get(fromType).increment(amount); } + /** + * Returns a List of player hands. This is ordered by player ID. + */ public List> getPlayerHands() { return playerHands; } @@ -190,6 +185,9 @@ protected double _getHeuristicScore(int playerId) { } @Override + /** + * Tie break is the number of puddings + */ public double getTiebreak(int playerId, int tier) { // Tie-break is number of puddings return playedCardTypes[playerId].get(SGCard.SGCardType.Pudding).getValue(); @@ -200,10 +198,18 @@ public double getGameScore(int playerId) { return playerScore[playerId].getValue(); } + /** + * Returns a Map from CardType to the number of times that card type has been played by the player. + * This only includes the current round. + */ public Map[] getPlayedCardTypes() { return playedCardTypes; } + /** + * Returns a Map from CardType to the number of times that card type has been played by the player. + * This is over the whole game. + */ public Map[] getPlayedCardTypesAllGame() { return playedCardTypesAllGame; } @@ -212,10 +218,17 @@ public Map[] getPointsPerCardType() { return pointsPerCardType; } + /** + * Returns the number of times a card type has been played by a player in the current round. + * (Value returned in a Counter object) + */ public Counter getPlayedCardTypes(SGCard.SGCardType cardType, int player) { return playedCardTypes[player].get(cardType); } + /** + * The Deck of all played cards + */ public List> getPlayedCards() { return playedCards; } diff --git a/src/main/java/games/sushigo/gui/SGGUIManager.java b/src/main/java/games/sushigo/gui/SGGUIManager.java index 582a5bdf2..94c3dc14b 100644 --- a/src/main/java/games/sushigo/gui/SGGUIManager.java +++ b/src/main/java/games/sushigo/gui/SGGUIManager.java @@ -149,7 +149,7 @@ protected JPanel createGameStateInfoPanel(String gameTitle, AbstractGameState ga wrapper.setLayout(new FlowLayout()); wrapper.add(gameInfo); - createActionHistoryPanel(width / 2 - 10, height, humanPlayerId); + createActionHistoryPanel(width / 2 - 10, height, humanPlayerIds); wrapper.add(historyContainer); return wrapper; } @@ -174,7 +174,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { for (int i = 0; i < gameState.getNPlayers(); i++) { playerHands[i].update(parsedGameState); if (i == gameState.getCurrentPlayer() - || humanPlayerId.contains(i)) { + || humanPlayerIds.contains(i)) { playerHands[i].playerHandView.setFront(true); playerHands[i].setFocusable(true); } else { diff --git a/src/main/java/games/tictactoe/TicTacToeConstants.java b/src/main/java/games/tictactoe/TicTacToeConstants.java index 6ff182634..94c3f5622 100644 --- a/src/main/java/games/tictactoe/TicTacToeConstants.java +++ b/src/main/java/games/tictactoe/TicTacToeConstants.java @@ -5,7 +5,7 @@ import java.util.ArrayList; public class TicTacToeConstants { - public static final ArrayList playerMapping = new ArrayList() {{ + public static final ArrayList playerMapping = new ArrayList<>() {{ add(new Token("x")); add(new Token("o")); }}; diff --git a/src/main/java/games/tictactoe/TicTacToeGameState.java b/src/main/java/games/tictactoe/TicTacToeGameState.java index ad92c6538..9e7e0619f 100644 --- a/src/main/java/games/tictactoe/TicTacToeGameState.java +++ b/src/main/java/games/tictactoe/TicTacToeGameState.java @@ -31,7 +31,7 @@ protected GameType _getGameType() { @Override protected List _getAllComponents() { - return new ArrayList() {{ + return new ArrayList<>() {{ add(gridBoard); }}; } @@ -49,9 +49,7 @@ protected double _getHeuristicScore(int playerId) { } /** - * This provides the current score in game turns. This will only be relevant for games that have the concept - * of victory points, etc. - * If a game does not support this directly, then just return 0.0 + * For TicTacToe this returns 0 unless the game is over. In which case 1 is a win, 0.5 is a draw and 0 is a loss. * * @param playerId * @return - double, score of current state @@ -61,6 +59,14 @@ public double getGameScore(int playerId) { return playerResults[playerId].value; } + /** + * This returns the player id of the token at the given position. Or -1 if this is empty. + */ + public int getPlayerAt(int x, int y) { + Token token = gridBoard.getElement(x, y); + return token == null ? -1 : token.getOwnerId(); + } + @Override protected boolean _equals(Object o) { if (this == o) return true; diff --git a/src/main/java/games/uno/gui/UnoGUIManager.java b/src/main/java/games/uno/gui/UnoGUIManager.java index 523f6ce4e..a471de207 100644 --- a/src/main/java/games/uno/gui/UnoGUIManager.java +++ b/src/main/java/games/uno/gui/UnoGUIManager.java @@ -138,7 +138,7 @@ protected void _update(AbstractPlayer player, AbstractGameState gameState) { for (int i = 0; i < gameState.getNPlayers(); i++) { playerHands[i].update((UnoGameState) gameState); if (i == gameState.getCurrentPlayer() && gameState.getCoreGameParameters().alwaysDisplayCurrentPlayer - || humanPlayerId.contains(i) + || humanPlayerIds.contains(i) || gameState.getCoreGameParameters().alwaysDisplayFullObservable) { playerHands[i].playerHandView.setFront(true); playerHands[i].setFocusable(true); diff --git a/src/main/java/games/wonders7/Wonders7GameState.java b/src/main/java/games/wonders7/Wonders7GameState.java index 76fb19261..11fbb9f5b 100644 --- a/src/main/java/games/wonders7/Wonders7GameState.java +++ b/src/main/java/games/wonders7/Wonders7GameState.java @@ -107,7 +107,7 @@ public AbstractGameState _copy(int playerId) { for (int i = 0; i < getNPlayers(); i++) { - if (!hasSeenHand(playerId, i)) { + if (!isHandKnownTo(playerId, i)) { copy.ageDeck.add(copy.playerHands.get(i)); // Groups other players cards (except for next players hand) into the ageDeck // (along with any cards that were not in the game at that age) @@ -116,7 +116,7 @@ public AbstractGameState _copy(int playerId) { copy.ageDeck.add(copy.discardPile); // Groups the discard pile into the ageDeck copy.ageDeck.shuffle(redeterminisationRnd); // Shuffle all the cards for (int i = 0; i < getNPlayers(); i++) { - if (!hasSeenHand(playerId, i)) { + if (!isHandKnownTo(playerId, i)) { Deck hand = copy.playerHands.get(i); int nCards = hand.getSize(); hand.clear(); // Empties the accurate player hands, except for the next players hand @@ -147,7 +147,7 @@ public AbstractGameState _copy(int playerId) { * @param opponentId - id of opponent owning the hand of cards we're checking vision of * @return - true if player has seen the opponent's hand of cards, false otherwise */ - public boolean hasSeenHand(int playerId, int opponentId) { + public boolean isHandKnownTo(int playerId, int opponentId) { if (playerId == opponentId) return true; // always know your own hand Wonders7GameParameters params = (Wonders7GameParameters) gameParameters; // a player knows a number of other hands equal to the number of cards they have played @@ -229,15 +229,16 @@ public List> getPlayerHands() { return playerHands; } - public Deck getPlayedCards(int index) { - return playedCards.get(index); - } // Get player 'played' cards + // Cards played to the specified player's tableau + public Deck getPlayedCards(int playerId) { + return playedCards.get(playerId); + } public Deck getDiscardPile() { return discardPile; } - public AbstractAction getTurnAction(int index) { + AbstractAction getTurnAction(int index) { return turnActions[index]; } @@ -245,23 +246,20 @@ public void setTurnAction(int index, AbstractAction action) { turnActions[index] = action; } - public Wonder7Board getPlayerWonderBoard(int index) { - return playerWonderBoard[index]; + public Wonder7Board getPlayerWonderBoard(int playerId) { + return playerWonderBoard[playerId]; } - public void setPlayerWonderBoard(int index, Wonder7Board wonder) { - playerWonderBoard[index] = wonder; + public void setPlayerWonderBoard(int playerId, Wonder7Board wonder) { + playerWonderBoard[playerId] = wonder; } - - public List> getAllPlayerResources() { - return playerResources; - } // Return all player's resources hashmap - - public HashMap getPlayerResources(int index) { - return playerResources.get(index); + // A summary Map of all the resources a player has from their played cards and Wonder Board + public HashMap getPlayerResources(int playerId) { + return playerResources.get(playerId); } // Return players resource hashmap + // The number of Resources of the specified type a player has public int getResource(int player, Wonders7Constants.Resource resource) { return playerResources.get(player).get(resource); } diff --git a/src/main/java/gui/AbstractGUIManager.java b/src/main/java/gui/AbstractGUIManager.java index 4abb2c8ed..55aea398e 100644 --- a/src/main/java/gui/AbstractGUIManager.java +++ b/src/main/java/gui/AbstractGUIManager.java @@ -22,7 +22,7 @@ public abstract class AbstractGUIManager { protected GamePanel parent; protected Game game; - protected Set humanPlayerId; + protected Set humanPlayerIds; public static int defaultItemSize = 50; public static int defaultActionPanelHeight = 100; @@ -50,7 +50,7 @@ public AbstractGUIManager(GamePanel parent, Game game, ActionController ac, Set< this.maxActionSpace = getMaxActionSpace(); this.parent = parent; this.game = game; - this.humanPlayerId = human; + this.humanPlayerIds = human; gameStatus = new JLabel(); playerStatus = new JLabel(); @@ -167,8 +167,8 @@ public ActionController getAC() { return ac; } - public Set getHumanPlayerId() { - return humanPlayerId; + public Set getHumanPlayerIds() { + return humanPlayerIds; } /** @@ -198,7 +198,7 @@ protected JPanel createGameStateInfoPanel(String gameTitle, AbstractGameState ga wrapper.setLayout(new FlowLayout()); wrapper.add(gameInfo); - createActionHistoryPanel(width / 2 - 10, height, humanPlayerId); + createActionHistoryPanel(width / 2 - 10, height, humanPlayerIds); wrapper.add(historyContainer); return wrapper; } diff --git a/src/main/java/gui/LLMFrontend.java b/src/main/java/gui/LLMFrontend.java new file mode 100644 index 000000000..b723f6876 --- /dev/null +++ b/src/main/java/gui/LLMFrontend.java @@ -0,0 +1,852 @@ +package gui; + +import com.formdev.flatlaf.FlatDarculaLaf; +import core.*; +import core.actions.AbstractAction; +import core.interfaces.IStateHeuristic; +import evaluation.RunArg; +import evaluation.listeners.MetricsGameListener; +import evaluation.metrics.Event; +import evaluation.optimisation.TunableParameters; +import evaluation.tournaments.AbstractTournament; +import evaluation.tournaments.RoundRobinTournament; +import games.GameType; +import gui.models.AITableModel; +import players.PlayerParameters; +import players.PlayerType; +import players.heuristics.StateHeuristicType; +import players.heuristics.StringHeuristic; +import players.human.ActionController; + +import javax.swing.Timer; +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.io.*; +import java.util.List; +import java.util.*; + +public class LLMFrontend extends GUI { + private final int nMaxPlayers = 20; + private final int defaultNPlayers = 2; + Timer guiUpdater; + JFrame[] gameParameterEditWindow, playerParameterEditWindow; + PlayerParameters[] playerParameters, agentParameters; + JButton[] editHeuristicButtons; + private Thread gameThread; + private Game gameRunning; + private boolean showAll, paused, started, showAIWindow; + private ActionController humanInputQueue; + + public LLMFrontend() { + + try { + UIManager.setLookAndFeel(new FlatDarculaLaf()); + } catch (UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } + + // Number of players selection + JPanel playerSelect = new JPanel(); + playerSelect.setLayout(new BoxLayout(playerSelect, BoxLayout.Y_AXIS)); + JPanel nPlayers = new JPanel(new BorderLayout(5, 5)); + playerSelect.add(nPlayers); + JLabel nPlayersText = new JLabel(" # players (max " + nMaxPlayers + "):"); + nPlayers.add(BorderLayout.WEST, nPlayersText); + JTextField nPlayerField = new JTextField("" + defaultNPlayers, 10); // integer of this is n players + nPlayers.add(BorderLayout.CENTER, nPlayerField); + + // Game type and parameters selection + JPanel gameSelect = new JPanel(new BorderLayout(5, 5)); + gameSelect.add(BorderLayout.WEST, new JLabel(" Game type:")); + String[] gameNames = new String[GameType.values().length]; + TunableParameters[] gameParameters = new TunableParameters[GameType.values().length]; + gameParameterEditWindow = new JFrame[GameType.values().length]; + for (int i = 0; i < gameNames.length; i++) { + gameNames[i] = GameType.values()[i].name(); + AbstractParameters params = GameType.values()[i].createParameters(0); + if (params instanceof TunableParameters) { + gameParameters[i] = (TunableParameters) params; + gameParameterEditWindow[i] = new JFrame(); + gameParameterEditWindow[i].getContentPane().setLayout(new BoxLayout(gameParameterEditWindow[i].getContentPane(), BoxLayout.Y_AXIS)); + + List paramNames = gameParameters[i].getParameterNames(); + HashMap> paramValueOptions = createParameterWindow(paramNames, gameParameters[i], gameParameterEditWindow[i]); + + int idx = i; + JButton submit = new JButton("Submit"); + submit.addActionListener(e -> { + for (String param : paramNames) { + gameParameters[idx].setParameterValue(param, paramValueOptions.get(param).getSelectedItem()); + } + gameParameterEditWindow[idx].dispose(); + }); + JButton reset = new JButton("Reset"); + reset.addActionListener(e -> { + gameParameters[idx].reset(); + for (String param : paramNames) { + paramValueOptions.get(param).setSelectedItem(gameParameters[idx].getDefaultParameterValue(param)); + } + }); + JPanel buttons = new JPanel(); + buttons.add(submit); + buttons.add(reset); + + gameParameterEditWindow[i].getContentPane().add(buttons); + } + } +// GameType firstGameType = GameType.valueOf(gameNames[0]); + GameType firstGameType = GameType.LoveLetter; + + nPlayersText.setText(" # players (min " + firstGameType.getMinPlayers() + ", max " + firstGameType.getMaxPlayers() + "):"); + + JComboBox gameOptions = new JComboBox<>(gameNames); // index of this selection is game + gameSelect.add(BorderLayout.CENTER, gameOptions); + JButton gameParameterEdit = new JButton("Edit"); + gameOptions.addActionListener(e -> { + int idx = gameOptions.getSelectedIndex(); + gameParameterEdit.setVisible(gameParameterEditWindow[idx] != null); + + GameType gameType = GameType.valueOf((String) gameOptions.getSelectedItem()); + nPlayersText.setText(" # players (min " + gameType.getMinPlayers() + ", max " + gameType.getMaxPlayers() + "):"); + pack(); + }); + gameSelect.add(BorderLayout.EAST, gameParameterEdit); + gameParameterEdit.addActionListener(e -> { + int idx = gameOptions.getSelectedIndex(); + if (gameParameterEditWindow[idx] != null) { + gameParameterEditWindow[idx].pack(); + gameParameterEditWindow[idx].setDefaultCloseOperation(DISPOSE_ON_CLOSE); + gameParameterEditWindow[idx].setTitle("Edit parameters " + gameOptions.getSelectedItem()); + gameParameterEditWindow[idx].setVisible(true); + } + }); + + // For each player, select type and parameters + JPanel[] playerOptions = new JPanel[nMaxPlayers]; + JComboBox[] playerOptionsChoice = new JComboBox[nMaxPlayers]; // player is index of this selection + String[] playerOptionsString = new String[PlayerType.values().length]; + // agentParameters contains the defaults (last edited set) of parameters for each agent type + agentParameters = new PlayerParameters[PlayerType.values().length]; + for (int i = 0; i < playerOptionsString.length; i++) { + playerOptionsString[i] = PlayerType.values()[i].name(); + agentParameters[i] = PlayerType.values()[i].createParameterSet(); + } + // We have one JFrame per player, as different players may use the same agent type with different parameters + playerParameters = new PlayerParameters[nMaxPlayers]; + playerParameterEditWindow = new JFrame[nMaxPlayers]; + editHeuristicButtons = new JButton[nMaxPlayers]; + for (int i = 0; i < nMaxPlayers; i++) { + int playerIdx = i; + playerOptions[i] = new JPanel(new BorderLayout(5, 5)); + if (i >= defaultNPlayers) { + playerOptions[i].setVisible(false); + } + playerOptions[i].add(BorderLayout.WEST, new JLabel(" Player " + i + ":")); + JButton paramButton = new JButton("Edit"); + paramButton.setVisible(false); + + JButton fileButton = new JButton("Load JSON"); + fileButton.setVisible(false); + + // Add a button to view and edit the String Heuristic text in filePath + editHeuristicButtons[i] = new JButton("Edit Heuristic"); + editHeuristicButtons[i].setVisible(false); + int pId = i; + editHeuristicButtons[i].addActionListener(ex -> { + if (playerParameters[pId] != null) { + IStateHeuristic h = playerParameters[pId].getStateHeuristic(); + if (h instanceof StringHeuristic heuristic) { + String filePath = heuristic.getFileName(); + JFrame editFrame = new JFrame(); + editFrame.getContentPane().setLayout(new BoxLayout(editFrame.getContentPane(), BoxLayout.Y_AXIS)); + editFrame.setTitle("Edit " + filePath); + JTextArea textArea = new JTextArea(); + textArea.setText(heuristic.getHeuristicCode()); + JScrollPane scrollPane = new JScrollPane(textArea); + scrollPane.setPreferredSize(new Dimension(800, 500)); + scrollPane.getVerticalScrollBar().setUnitIncrement(16); + scrollPane.getVerticalScrollBar().setBlockIncrement(100); + editFrame.getContentPane().add(scrollPane); + JButton saveButton = new JButton("Save"); + saveButton.setEnabled(false); + saveButton.setAlignmentX(Component.CENTER_ALIGNMENT); + saveButton.addActionListener(e1 -> { + BufferedWriter writer = null; + try { + writer = new BufferedWriter(new FileWriter(filePath)); + writer.write(textArea.getText()); + } catch (IOException ioException) { + ioException.printStackTrace(); + } finally { + try { + if (writer != null) { + writer.close(); + } + } catch (IOException ioException) { + ioException.printStackTrace(); + } + } + heuristic.setHeuristicCode(textArea.getText()); + saveButton.setEnabled(false); + }); + editFrame.getContentPane().add(saveButton); + + textArea.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + saveButton.setEnabled(true); + } + + @Override + public void removeUpdate(DocumentEvent e) { + saveButton.setEnabled(true); + } + + @Override + public void changedUpdate(DocumentEvent e) { + saveButton.setEnabled(true); + } + }); + + editFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + editFrame.setSize(800, 600); + editFrame.setVisible(true); + } + } + }); + // todo button should be disabled if changing heuristic or agent + + JPanel paramButtonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + paramButtonPanel.add(paramButton); + paramButtonPanel.add(fileButton); + paramButtonPanel.add(editHeuristicButtons[i]); + + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.addChoosableFileFilter( + new FileNameExtensionFilter("JSON files only", "json") + ); + + fileButton.addActionListener(e -> { + int retVal = fileChooser.showOpenDialog(this); + if (retVal == JFileChooser.APPROVE_OPTION) { + try { + PlayerParameters.loadFromJSONFile(playerParameters[playerIdx], fileChooser.getSelectedFile().getPath()); + } catch (Exception loadingException) { + System.out.println("File not loadable : " + loadingException.getMessage()); + int agentIdx = playerOptionsChoice[playerIdx].getSelectedIndex(); + playerParameters[playerIdx] = (PlayerParameters) agentParameters[agentIdx].copy(); + } + } + } + ); + + playerOptionsChoice[i] = new JComboBox<>(playerOptionsString); + playerOptionsChoice[i].setSelectedItem("Random"); + + playerParameterEditWindow[i] = new JFrame(); + playerParameterEditWindow[i].getContentPane().setLayout(new BoxLayout(playerParameterEditWindow[i].getContentPane(), BoxLayout.Y_AXIS)); + + playerOptionsChoice[i].addActionListener(e -> { + int agentIndex = playerOptionsChoice[playerIdx].getSelectedIndex(); + // set Edit button visible if we have parameters to edit; else make it invisible + paramButton.setVisible(agentParameters[agentIndex] != null); + fileButton.setVisible(agentParameters[agentIndex] != null); + // set up the player parameters with the current saved default for that agent type + + paramButton.removeAll(); + try { + playerParameters[playerIdx] = (PlayerParameters) agentParameters[agentIndex].copy(); + paramButton.addActionListener(f -> { + initialisePlayerParameterWindow(playerIdx, agentIndex); + playerParameterEditWindow[playerIdx].setTitle("Edit parameters " + playerOptionsChoice[playerIdx].getSelectedItem()); + playerParameterEditWindow[playerIdx].pack(); + playerParameterEditWindow[playerIdx].setVisible(true); + playerParameterEditWindow[playerIdx].setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + }); + } catch (Exception ignored) {} + pack(); + }); + playerOptions[i].add(BorderLayout.CENTER, playerOptionsChoice[i]); + playerOptions[i].add(BorderLayout.EAST, paramButtonPanel); + playerSelect.add(playerOptions[i]); + } + JButton updateNPlayers = new JButton("Update"); + updateNPlayers.addActionListener(e -> { + if (!nPlayerField.getText().equals("")) { + int nP = Integer.parseInt(nPlayerField.getText()); + if (nP > 0 && nP < nMaxPlayers) { + for (int i = 0; i < nP; i++) { + playerOptions[i].setVisible(true); + } + for (int i = nP; i < nMaxPlayers; i++) { + playerOptions[i].setVisible(false); + } + pack(); + } else { + JOptionPane.showMessageDialog(null, + "Error: Please enter number bigger than 0 and less than " + nMaxPlayers, "Error Message", + JOptionPane.ERROR_MESSAGE); + } + } + }); + nPlayers.add(BorderLayout.EAST, updateNPlayers); + + // Select visuals on/off + JPanel visualSelect = new JPanel(new BorderLayout(5, 5)); + visualSelect.add(BorderLayout.WEST, new JLabel(" Visuals ON/OFF:")); + JComboBox visualOptions = new JComboBox<>(new Boolean[]{false, true}); // index here is visuals on/off + visualOptions.setSelectedItem(true); + visualSelect.add(BorderLayout.EAST, visualOptions); + + JPanel turnPause = new JPanel(new BorderLayout(5, 5)); + turnPause.add(BorderLayout.WEST, new JLabel(" Pause between turns (ms):")); + JTextField turnPauseValue = new JTextField(" 200"); + turnPause.add(BorderLayout.EAST, turnPauseValue); + + // Submit final feedback to LLM bot + JTextArea feedback = new JTextArea(""); + feedback.setLineWrap(true); + feedback.setWrapStyleWord(true); +// feedback.setPreferredSize(new Dimension(300, 100)); + JScrollPane spFeedback = new JScrollPane(feedback); + spFeedback.setPreferredSize(new Dimension(300, 150)); + spFeedback.getVerticalScrollBar().setUnitIncrement(16); // Default is usually 1-5 + spFeedback.getVerticalScrollBar().setBlockIncrement(100); // Default is usually larger + JButton submitFeedback = new JButton("Submit Feedback"); + submitFeedback.addActionListener(e -> { + System.out.println("Feedback: " + feedback.getText()); + feedback.setText(""); + }); + submitFeedback.setEnabled(false); + submitFeedback.setAlignmentX(Component.CENTER_ALIGNMENT); + + // Console output logs + JTextArea output = new JTextArea(""); + output.setLineWrap(true); + output.setWrapStyleWord(true); +// output.setPreferredSize(new Dimension(300, 50)); + output.setEditable(false); + JScrollPane spOutput = new JScrollPane(output); + spOutput.setPreferredSize(new Dimension(300, 150)); + spOutput.getVerticalScrollBar().setUnitIncrement(16); // Default is usually 1-5 + spOutput.getVerticalScrollBar().setBlockIncrement(100); // Default is usually larger + redirectSystemStreams(output); + + // Select random seed + JPanel seedSelect = new JPanel(new BorderLayout(5, 5)); + seedSelect.add(BorderLayout.WEST, new JLabel(" Seed:")); + JTextField seedOption = new JTextField("" + System.currentTimeMillis()); // integer of this is seed + JButton seedRefresh = new JButton("Refresh"); + seedRefresh.addActionListener(e -> seedOption.setText("" + System.currentTimeMillis())); + seedSelect.add(BorderLayout.CENTER, seedOption); + seedSelect.add(BorderLayout.EAST, seedRefresh); + + // Select n games to run + JPanel nGamesSelect = new JPanel(new BorderLayout(5, 5)); + nGamesSelect.add(BorderLayout.WEST, new JLabel(" N Games:")); + JTextField nGamesOption = new JTextField("1"); + nGamesSelect.add(BorderLayout.CENTER, nGamesOption); + + // Game run core parameters select + CoreParameters coreParameters = new CoreParameters(); + JPanel gameRunParamSelect = new JPanel(); + gameRunParamSelect.setLayout(new BoxLayout(gameRunParamSelect, BoxLayout.Y_AXIS)); + HashMap> coreParameterValueOptions = new HashMap<>(); + for (String param : coreParameters.getParameterNames()) { + JPanel paramPanel = new JPanel(); + paramPanel.setLayout(new BorderLayout(5, 5)); + paramPanel.add(BorderLayout.WEST, new JLabel(String.format(" %-40s", param))); + paramPanel.add(BorderLayout.CENTER, new JPanel()); + List values = coreParameters.getPossibleValues(param); + JComboBox valueOptions = new JComboBox<>(values.toArray()); + valueOptions.setSelectedItem(coreParameters.getDefaultParameterValue(param)); + coreParameterValueOptions.put(param, valueOptions); + paramPanel.add(BorderLayout.EAST, valueOptions); + gameRunParamSelect.add(leftJustify(paramPanel)); + } + + // Empty panel to hold game when play button is pressed + GamePanel gamePanel = new GamePanel(); + gamePanel.setVisible(false); + + JPanel gameControlButtons = new JPanel(); + // Analysis button + JButton AIAnalysis = new JButton("AI Window OFF"); + AIAnalysis.setToolTipText("Click to Toggle. If ON, pop-up window shows AI decision statistics prior to each decision."); + AIAnalysis.setEnabled(false); + AIAnalysis.addActionListener(e -> { + showAIWindow = !showAIWindow; + AIAnalysis.setText(showAIWindow ? "AI Window ON" : "AI Window OFF"); + }); + AIAnalysis.setEnabled(false); + + JButton allActions = new JButton("Showing Self"); + allActions.setToolTipText("Click to Toggle. Either show actions for all players (All), or just those of a human player (Self)."); + allActions.addActionListener(e -> { + showAll = !showAll; + AIAnalysis.setEnabled(showAll); + if (!showAll) { + showAIWindow = false; + AIAnalysis.setText("AI Window OFF"); + } + allActions.setText(showAll ? "Showing All" : "Showing Self"); + }); + allActions.setEnabled(false); + + // Pause game button + JButton oneAction = new JButton("Next Action"); + oneAction.setToolTipText("Use to take the next AI action when the game is Paused."); + oneAction.setEnabled(paused && started); + + JButton startGame = new JButton("Play!"); + startGame.setToolTipText("Starts a game (if none running), or Stops a running game."); + + JButton pauseGame = new JButton("Pause"); + pauseGame.setEnabled(false); + pauseGame.setToolTipText("Toggles pause on and off. When Paused you can use NextAction to move through AI turns."); + pauseGame.addActionListener(e -> { + paused = !paused; + pauseGame.setText(paused ? "Resume" : "Pause"); + if (gameRunning != null) { + gameRunning.setPaused(paused); + if (!paused && !gameRunning.isHumanToMove()) { + // in this case we need to notify the game loop to get going again + synchronized (gameRunning) { + gameRunning.notifyAll(); + } + } + } + oneAction.setEnabled(paused && started); + }); + + java.awt.event.ActionListener stopTrigger = e -> { + if (gameRunning != null) { + gameRunning.setStopped(true); + if (guiUpdater != null) + guiUpdater.stop(); + gameThread.interrupt(); + guiUpdater.stop(); + } + }; + + // Play button, runs game in separate thread to allow for proper updates + java.awt.event.ActionListener startTrigger = e -> { + GUI frame = this; + Runnable runnable = () -> { + humanInputQueue = (visualOptions.getSelectedIndex() == 0) ? null : new ActionController(); + long seed = Long.parseLong(seedOption.getText()); + ArrayList players = new ArrayList<>(); + int nP = Integer.parseInt(nPlayerField.getText()); + String[] playerNames = new String[nP]; + for (int i = 0; i < nP; i++) { + AbstractPlayer player = PlayerType.valueOf(playerOptionsChoice[i].getItemAt(playerOptionsChoice[i].getSelectedIndex())) + .createPlayerInstance(seed, humanInputQueue, playerParameters[i]); + playerNames[i] = player.toString(); + players.add(player); + } + GameType gameType = GameType.valueOf(gameOptions.getItemAt(gameOptions.getSelectedIndex())); + System.out.println("Playing `" + gameType.name() + "` with players: " + Arrays.toString(playerNames)); + + gamePanel.removeAll(); + TunableParameters params = gameParameters[gameOptions.getSelectedIndex()]; + if (params != null) { + params.setRandomSeed(seed); + } + + int nGames = Integer.parseInt(nGamesOption.getText()); + + if (nGames > 1 && humanInputQueue == null) { + Map config = new HashMap<>(); + config.put(RunArg.matchups, nGames); + config.put(RunArg.budget, 100); // todo param? or from agent? + RoundRobinTournament tournament = new RoundRobinTournament(players, gameType, players.size(), params, config); + tournament.run(); + } else { + gameRunning = gameType.createGameInstance(players.size(), params); + + // GUI + AbstractGUIManager gui = (humanInputQueue != null) ? gameType.createGUIManager(gamePanel, gameRunning, humanInputQueue) : null; + + // Find core parameters + for (String param : coreParameterValueOptions.keySet()) { + coreParameters.setParameterValue(param, coreParameterValueOptions.get(param).getSelectedItem()); + } + gameRunning.setCoreParameters(coreParameters); + + for (int i = 0; i < nGames; i++) { + // Reset game instance, passing the players for this game + gameRunning.reset(players); + try { + gameRunning.setTurnPause(Integer.parseInt(turnPauseValue.getText().trim())); + } catch (NumberFormatException notAnInteger) { + // just proceed without collapsing in a heap + } + + if (gui != null) { + setFrameProperties(); + pack(); + guiUpdater = new Timer((int) coreParameters.frameSleepMS, event -> updateGUI(gui, frame)); + guiUpdater.start(); + } + + // if Pause button has been pressed, then pause at the start so we can track all actions + gameRunning.setPaused(paused); + // set up sample for the first action + listenForDecisions(); + try { + gameRunning.run(); + System.out.println("Game over: " + Arrays.toString(gameRunning.getGameState().getPlayerResults())); + if (gui != null) { + guiUpdater.stop(); + // and update GUI to final game state + updateGUI(gui, frame); + } + } catch (Exception ex) { + System.out.println("Error: " + ex.getMessage()); + } + } + } + + submitFeedback.setEnabled(true); + pauseGame.setEnabled(false); + started = false; + oneAction.setEnabled(false); + AIAnalysis.setEnabled(false); + allActions.setEnabled(false); + startGame.setText("Play!"); + }; + gameThread = new Thread(runnable); + gameThread.start(); + }; + + startGame.addActionListener(e -> { + started = !started; + if (started) { + startTrigger.actionPerformed(e); + } else { + stopTrigger.actionPerformed(e); + } + oneAction.setEnabled(paused && started); + pauseGame.setEnabled(started); + AIAnalysis.setEnabled(started); + allActions.setEnabled(started); + startGame.setText(started ? "Stop!" : "Play!"); + }); + + + oneAction.addActionListener(e -> { + if (paused && gameRunning != null && !gameRunning.isHumanToMove()) { + // if the thread is running and paused (or human to move) + // and then take a single action + // (as long as it is not a human to move...as in this case the GUI is already in control) + synchronized (gameRunning) { + gameRunning.oneAction(); + gameRunning.notifyAll(); + } + } + }); + + gameControlButtons.add(startGame); + gameControlButtons.add(pauseGame); + gameControlButtons.add(oneAction); + gameControlButtons.add(allActions); + gameControlButtons.add(AIAnalysis); + + + // Put all together + + JPanel gameOptionFullPanel = new JPanel(); + gameOptionFullPanel.setLayout(new BoxLayout(gameOptionFullPanel, BoxLayout.Y_AXIS)); + gameOptionFullPanel.add(leftJustify(gameSelect)); + gameOptionFullPanel.add(leftJustify(playerSelect)); + gameOptionFullPanel.add(leftJustify(visualSelect)); + gameOptionFullPanel.add(leftJustify(turnPause)); + gameOptionFullPanel.add(leftJustify(seedSelect)); + gameOptionFullPanel.add(leftJustify(nGamesSelect)); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(new JSeparator()); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); +// gameOptionFullPanel.add(gameRunParamSelect); +// gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); +// gameOptionFullPanel.add(new JSeparator()); +// gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(gameControlButtons); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(new JSeparator()); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + JLabel outputLabel = new JLabel("Console output:"); + outputLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + gameOptionFullPanel.add(outputLabel); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(spOutput); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(new JSeparator()); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + JLabel feedbackLabel = new JLabel("LLM Feedback Input Post-Game:"); + feedbackLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + gameOptionFullPanel.add(feedbackLabel); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(spFeedback); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + gameOptionFullPanel.add(Box.createRigidArea(new Dimension(0, 5))); + // Align button center + gameOptionFullPanel.add(submitFeedback); + + + // Collapse run settings panel + JButton toggleButton = new JButton("<<"); + toggleButton.addActionListener(e -> { + boolean visible = gameOptionFullPanel.isVisible(); + if (visible) { + gameOptionFullPanel.setVisible(false); + toggleButton.setText(">>"); + } else { + gameOptionFullPanel.setVisible(true); + toggleButton.setText("<<"); + } + pack(); + }); + + // Wrap all together + JPanel wrapper = new JPanel(); + wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS)); + wrapper.add(gameOptionFullPanel); + wrapper.add(toggleButton); + wrapper.add(gamePanel); + + JPanel mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + JLabel title = new JLabel("TAG:LLM Frontend"); + // Increase title font size + title.setFont(new Font("Serif", Font.BOLD, 20)); + title.setForeground(new Color(124, 241, 231, 190)); + title.setAlignmentX(Component.CENTER_ALIGNMENT); + + mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); + mainPanel.add(title); + mainPanel.add(Box.createRigidArea(new Dimension(0, 10))); + mainPanel.add(wrapper); + getContentPane().add(mainPanel, BorderLayout.CENTER); + gamePanel.revalidate(); + gamePanel.setVisible(true); + gamePanel.repaint(); + + // Frame properties + setFrameProperties(); + + pack(); + } + + private void redirectSystemStreams(JTextArea textArea) { + OutputStream out = new OutputStream() { + @Override + public void write(int b) { + // Append the single character to the JTextArea + textArea.append(String.valueOf((char) b)); + textArea.setCaretPosition(textArea.getDocument().getLength()); + } + + @Override + public void write(byte[] b, int off, int len) { + // Append the portion of the byte array to the JTextArea + textArea.append(new String(b, off, len)); + textArea.setCaretPosition(textArea.getDocument().getLength()); + } + + @Override + public void write(byte[] b) { + // Append the entire byte array to the JTextArea + write(b, 0, b.length); + } + }; + + System.setOut(new PrintStream(out, true)); + System.setErr(new PrintStream(out, true)); + } + + public static void main(String[] args) { + new LLMFrontend(); + + // todo pipeline, frontend-controlled. + // - some way of getting the base prompt for a game, and allow edit (maybe in GameType, have a file with the prompt) + // - for new games where we don't have this... input game manual (external doc) + API (game-specific) + structure (general) + // - select heuristic player, have button for 'attach LLM'. This will call the LLM with the initial prompt and load up the heuristic generated + // - we play the game, maybe several times, with human/other player, then submit feedback + // - the 'submit feedback' button not only prints feedback, but also iterates on heuristic function with LLM + // - print to console the extra fluff text generated maybe? + // - repeat! + + // todo + // - log interactions to file with timestamp (feedback sent, LLM complete output, game results). get some people to play + // - alternative: submit feedback mid-game. Pause game, iterate heuristic, resume game. + // - alternative: have the LLM iterate by itself for some generations, using automatic evaluation of heuristic + } + + private void initialisePlayerParameterWindow(int playerIndex, int agentIndex) { + if (playerParameters[playerIndex] != null) { + List paramNames = playerParameters[playerIndex].getParameterNames(); + HashMap> paramValueOptions = createParameterWindow(paramNames, playerParameters[playerIndex], playerParameterEditWindow[playerIndex]); + + JButton submit = new JButton("Submit"); + submit.addActionListener(e -> { + boolean foundStringHeuristic = false; + for (String param : paramNames) { + Object value = paramValueOptions.get(param).getSelectedItem(); + playerParameters[playerIndex].setParameterValue(param, value); + // we also update the central copy, so this change is inherited by future new players + + // Add a button to view and edit the String Heuristic text in filePath + if (value == StateHeuristicType.StringHeuristic) { + editHeuristicButtons[playerIndex].setVisible(true); + foundStringHeuristic = true; + } + } + agentParameters[agentIndex] = (PlayerParameters) playerParameters[playerIndex].copy(); + playerParameterEditWindow[playerIndex].dispose(); + if (!foundStringHeuristic) { + editHeuristicButtons[playerIndex].setVisible(false); + } + }); + JButton reset = new JButton("Reset"); + reset.addActionListener(e -> { + playerParameters[playerIndex].reset(); + PlayerParameters defaultParams = PlayerType.values()[agentIndex].createParameterSet(); + if (defaultParams != null) + for (String param : paramNames) { + paramValueOptions.get(param).setSelectedItem(defaultParams.getDefaultParameterValue(param)); + } + }); + JPanel buttons = new JPanel(); + buttons.add(submit); + buttons.add(reset); + + playerParameterEditWindow[playerIndex].getContentPane().add(buttons); + } else { + editHeuristicButtons[playerIndex].setVisible(false); + } + } + + private Component leftJustify(JPanel panel) { + Box b = Box.createHorizontalBox(); + b.add(panel); + panel.setAlignmentX(Component.LEFT_ALIGNMENT); + b.add(Box.createHorizontalGlue()); + return b; + } + + private void listenForDecisions() { + // add a listener to detect every time an action has been taken + gameRunning.addListener(new MetricsGameListener() { + @Override + public void onEvent(Event event) + { + if(event.type == Event.GameEvent.ACTION_TAKEN) + updateSampleActions(event.state.copy()); + } + }); + + // and then do this at the start of the game + updateSampleActions(gameRunning.getGameState()); + } + + private void updateSampleActions(AbstractGameState state) { + if (showAIWindow && state.isNotTerminal() && !gameRunning.isHumanToMove()) { + int nextPlayerID = state.getCurrentPlayer(); + AbstractPlayer nextPlayer = gameRunning.getPlayers().get(nextPlayerID); + nextPlayer.getAction(state, nextPlayer.getForwardModel().computeAvailableActions(state, nextPlayer.getParameters().actionSpace)); + + JFrame AI_debug = new JFrame(); + AI_debug.setTitle(String.format("Player %d, Tick %d, Round %d, Turn %d", + nextPlayerID, + gameRunning.getTick(), + state.getRoundCounter(), + state.getTurnCounter())); + Map> decisionStats = nextPlayer.getDecisionStats(); + if (decisionStats.size() > 1) { + AITableModel AIDecisions = new AITableModel(nextPlayer.getDecisionStats()); + JTable table = new JTable(AIDecisions); + table.setAutoCreateRowSorter(true); + table.setDefaultRenderer(Double.class, (table1, value, isSelected, hasFocus, row, column) -> new JLabel(String.format("%.2f", (Double) value))); + JScrollPane scrollPane = new JScrollPane(table); + table.setFillsViewportHeight(true); + AI_debug.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + AI_debug.add(scrollPane); + AI_debug.revalidate(); + AI_debug.pack(); + AI_debug.setVisible(true); + } + } + } + + + /** + * Performs GUI update. + * + * @param gui - gui to update. + */ + private void updateGUI(AbstractGUIManager gui, JFrame frame) { + AbstractGameState gameState = gameRunning.getGameState().copy(); + int currentPlayer = gameState.getCurrentPlayer(); + AbstractPlayer player = gameRunning.getPlayers().get(currentPlayer); + if (gui != null) { + gui.update(player, gameState, gameRunning.isHumanToMove() || showAll); + if (!gameRunning.isHumanToMove() && paused && showAll) { + // in this case we allow a human to override an AI decision + try { + if (humanInputQueue.hasAction()) { + gameRunning.getForwardModel().next(gameState, humanInputQueue.getAction()); + } + } catch (InterruptedException e) { + // Really shouldn't happen as we checked first + e.printStackTrace(); + } + } + if (!gameRunning.isHumanToMove()) + humanInputQueue.reset(); // clear out any actions clicked before their turn + frame.revalidate(); + frame.repaint(); + } + } + + private HashMap> createParameterWindow(List paramNames, TunableParameters pp, JFrame frame) { + HashMap> paramValueOptions = new HashMap<>(); + frame.getContentPane().removeAll(); + + JPanel wrapper = new JPanel(); + wrapper.setLayout(new BoxLayout(wrapper, BoxLayout.X_AXIS)); + int maxParamsPerPanel = 20; + int paramIdx = 0; + + while (paramIdx < paramNames.size()) { + JPanel currentPanel = new JPanel(); + currentPanel.setLayout(new BoxLayout(currentPanel, BoxLayout.Y_AXIS)); + + for (int i = paramIdx; i < paramIdx + maxParamsPerPanel && i < paramNames.size(); i++) { + JPanel paramPanel = new JPanel(new BorderLayout()); + paramPanel.setLayout(new BoxLayout(paramPanel, BoxLayout.X_AXIS)); + String param = paramNames.get(i); + paramPanel.add(new JLabel(" " + param)); + List values = pp.getPossibleValues(param); + JComboBox valueOptions = new JComboBox<>(values.toArray()); + valueOptions.setSelectedItem(pp.getParameterValue(param)); + paramValueOptions.put(param, valueOptions); + paramPanel.add(valueOptions); + + currentPanel.add(paramPanel); + } + + paramIdx += maxParamsPerPanel; + wrapper.add(currentPanel); + wrapper.add(Box.createRigidArea(new Dimension(10, 0))); + } + + frame.getContentPane().add(wrapper); + frame.pack(); + + return paramValueOptions; + } +} diff --git a/src/main/java/gui/views/DeckView.java b/src/main/java/gui/views/DeckView.java index ce2a630a9..7400182c5 100644 --- a/src/main/java/gui/views/DeckView.java +++ b/src/main/java/gui/views/DeckView.java @@ -26,13 +26,13 @@ public abstract class DeckView extends ComponentView { // card and display sizes protected int itemWidth, itemHeight; - public DeckView(int player, Deck d, boolean visible, int componentWidth, int componentHeight) { - this(player, d, visible, componentWidth, componentHeight, new Rectangle(0, 0, componentWidth, componentHeight)); + public DeckView(int humanPlayer, Deck d, boolean visible, int componentWidth, int componentHeight) { + this(humanPlayer, d, visible, componentWidth, componentHeight, new Rectangle(0, 0, componentWidth, componentHeight)); } - public DeckView(int player, Deck d, boolean visible, int componentWidth, int componentHeight, Rectangle display) { + public DeckView(int humanPlayer, Deck d, boolean visible, int componentWidth, int componentHeight, Rectangle display) { super(d, display.width, display.height); - this.humanId = player; + this.humanId = humanPlayer; this.itemHeight = componentHeight; this.itemWidth = componentWidth; this.rect = display; diff --git a/src/main/java/llm/DocumentSummariser.java b/src/main/java/llm/DocumentSummariser.java new file mode 100644 index 000000000..54ac83307 --- /dev/null +++ b/src/main/java/llm/DocumentSummariser.java @@ -0,0 +1,134 @@ +package llm; + +import games.GameType; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.text.PDFTextStripper; + +import java.io.File; +import java.io.IOException; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class DocumentSummariser { + + private String documentProcessedText; + + public DocumentSummariser(String filePath) { + + String documentFullText; + if (filePath.endsWith("pdf")) { + try { + // Load the PDF document + PDDocument document = PDDocument.load(new File(filePath)); + + // Instantiate PDFTextStripper to extract text + PDFTextStripper pdfStripper = new PDFTextStripper(); + + // Retrieve text from the PDF + documentFullText = pdfStripper.getText(document); + // Close the document + document.close(); + } catch (IOException e) { + throw new RuntimeException("Error reading PDF file: " + filePath, e); + } + } else { + // Read in with Scanner + Scanner scanner = new Scanner(filePath); + StringBuilder sb = new StringBuilder(); + while (scanner.hasNextLine()) { + sb.append(scanner.nextLine()).append("\n"); + } + documentFullText = sb.toString(); + } + + // Process for line breaks + + StringBuilder result = new StringBuilder(); + String[] lines = documentFullText.split("\r?\n"); + Pattern numberPattern = Pattern.compile("\\d+"); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i].trim(); + if (numberPattern.matcher(line).matches()) { + // If the line contains only a number, keep the line break only + result.append("\n"); + } else { + // If the line does not contain only a number, remove the line break + result.append(line); + // Check if the next line exists and is not a number, if so, add a space + if (i + 1 < lines.length && !numberPattern.matcher(lines[i + 1].trim()).matches()) { + result.append(" "); + } + } + } + documentProcessedText = result.toString(); + if (documentProcessedText.startsWith("\n")) { + documentProcessedText = documentProcessedText.substring(1); + } + } + + public String processText() { + return processText("Game Rules", 500); + } + + // Removes page numbers and replaces with new paragraphs instead. Removes line breaks otherwise + // to avoid artificial PDF line breaks. Caveat: also removes line breaks we might want... + public String processText(String areaOfInterest, int wordLimit) { + + String queryPrompt = String.format(""" + Does this section of the document contain information about %s? + Rate this section from 1 to 5, where 1 is not at all and 5 is very much. + Just return a single number between 1 and 5. + Do not include any explanation. + """, areaOfInterest); + + String summaryPrompt = String.format(""" + Summarise the information about %s in this text in 1-2 sentences. + Be concise and clear. + """, areaOfInterest); + + String finalSummary = String.format(""" + Summarise the information about %s below in no more than %d words. + This summary will be used by a developer to implement the game. + Be concise and clear. + """, areaOfInterest, wordLimit); + + LLMAccess llm = new LLMAccess(LLMAccess.LLM_MODEL.MISTRAL, LLMAccess.LLM_SIZE.SMALL, "RulesSummary_LLM_Log.txt"); + + int charactersPerRequest = 2500; + int characterOverlap = 500; + int totalLength = documentProcessedText.length(); + int start = 0; + StringBuilder summary = new StringBuilder(); + while (start < totalLength) { + int end = start + charactersPerRequest; + String text = documentProcessedText.substring(start, Math.min(end, totalLength)); + String prompt = queryPrompt + "\n" + text; + String response = llm.getResponse(prompt); + int responseInt = 0; + try { + responseInt = Integer.parseInt(response.trim().substring(0, 1)); + } catch (NumberFormatException e) { + System.out.println("Invalid response: " + response); + responseInt = 1; + } + if (responseInt >= 3) { + response = llm.getResponse(summaryPrompt + "\n" + text); + summary.append(response); + } + start = end - characterOverlap; + } + + // Now ask for the final summary + return llm.getResponse(finalSummary + "\n" + summary); + } + + public static void main(String[] args) { + DocumentSummariser summariser = new DocumentSummariser("data/loveletter/rulebook.pdf"); + System.out.println(summariser.processText()); + + System.out.println(GameType.LoveLetter.loadRulebook()); + } +} + + diff --git a/src/main/java/llm/GamePromptGenerator.java b/src/main/java/llm/GamePromptGenerator.java new file mode 100644 index 000000000..058973d2a --- /dev/null +++ b/src/main/java/llm/GamePromptGenerator.java @@ -0,0 +1,355 @@ +package llm; + +import com.github.javaparser.JavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import games.GameType; + +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.*; +import java.util.*; + +public class GamePromptGenerator { + public enum TaskType { + Heuristic; + + public String getTaskTest(GameType gameType, int nPlayers, String className) { + if (this == Heuristic) { + return "You are playing the board game " + gameType.name() + ". Your job is to write the evaluation logic to help an AI play this game. " + + "Don't leave parts unfinished or TODOs. Do not include any package name.\n" + + "Write it all in a Java class called " + className + ", with only a single function with this signature:\n" + + "\tpublic double evaluateState(core.AbstractGameState gameState, int playerId)\n" + + "The variable gameState is the current state of the game, and playerId is the ID of the player we evaluate the state for. " + + "The return value is an estimate of the value of the state. This must be between 0.0 and 1.0. " + + "0.0 means we have no chance of winning, 0.50 means we have a 50% chance of winning, and 1.0 means we have won the game.\n" + + "You do not need to check for the end of the game, as the game engine will do that and only call evaluateState() if the game is still in progress.\n" + + "The first thing you'll do is cast the abstract game state variable " + + "to the specific one we need: " + gameType.getGameStateClass().getSimpleName() + ".\n Write the contents of this function, so that we give a higher numeric " + + "evaluation to those game states that are beneficial to playerId. " + + "Ths first player has a playerId of 0, the second player has a playerId of 1, and so on. " + + "There are a total of " + nPlayers + " players in the game.\n"; + } + return ""; + } + } + + // Packages to ignore when extracting methods and javadoc to pass to the LLM + static List packagesToIgnore = List.of("java.lang", "java.util", "core", "core.actions", "core.components", "evaluation.optimisation", + "java.util.function", "java.util.stream", "java.awt"); + // ... except for these classes in those otherwise ignored packages + static List classesToOverride = List.of("GridBoard", "Deck", "PartialObservableDeck", + "GraphBoardWithEdges", "BoardNodeWithEdges", + "Dice", "FrenchCard"); + + public String createLLMTaskPrompt(TaskType taskType, GameType gameType, int nPlayers, String className, boolean includeRules) { + StringBuilder result = new StringBuilder(); + + // Task information + result.append("This is your task: \n").append(taskType.getTaskTest(gameType, nPlayers, className)); + + if (includeRules) { + // Rulebook manual + String rules = gameType.loadRulebook(); + result.append("This is the description of the board game ").append(gameType.name()).append(": \n").append(rules).append("\n"); + } + // API, game-type specific + result.append("\nAs well as standard java libraries you must use the following API to complete the task:\n"); + + // Extract methods using reflection + List classData = getAllMethods(gameType.getGameStateClass()); + + for (ClassData data : classData) { + // First, extract Javadocs using JavaParser + String fullClassName = data.clazz.getPackageName() + "." + data.clazz.getSimpleName(); + File sourceFile = new File("src/main/java/" + fullClassName.replaceAll("\\.", "/") + ".java"); + Map javadocs = extractJavadocs(sourceFile); + + result.append("\nClass API for ").append(data.clazz.getTypeName()); + // then check for generics in definition + if (data.clazz.getTypeParameters().length > 0) { + result.append("<"); + for (int i = 0; i < data.clazz.getTypeParameters().length; i++) { + if (i > 0) { + result.append(", "); + } + result.append(data.clazz.getTypeParameters()[i].getGenericDeclaration().getSimpleName()); + } + result.append(">"); + } + + if (data.superClass != null && !data.superClass.equals("java.lang.Object")) { + result.append(" extends ").append(data.superClass); + } + result.append("\n"); + // if (!data.implementedInterfaces.isEmpty()) { + // result.append("Implements: ").append(data.implementedInterfaces).append("\n"); + // } + if (javadocs != null && javadocs.containsKey(data.clazz.getSimpleName())) { + result.append("Javadoc: ").append(javadocs.get(data.clazz.getSimpleName())).append("\n"); + } + if (data.classEnumsAsString != null) { + result.append("Enum Values: ").append(data.classEnumsAsString).append("\n"); + } + // Check for any public fields (excluding any enums) + for (Field field : data.clazz.getDeclaredFields()) { + if (Modifier.isPublic(field.getModifiers()) && !field.isEnumConstant()) { + result.append("\tpublic ").append(field.getType().getSimpleName()).append(" ").append(field.getName()).append("\n"); + } + } + // then add public methods + for (Method method : data.classMethods) { + if (method.getModifiers() == Modifier.PUBLIC) { + String signature = getMethodSignature(method); + if (javadocs != null && javadocs.containsKey(signature)) { + result.append("\tpublic ").append(signature).append(": ").append(javadocs.get(signature)).append("\n"); + } else { + result.append("\tpublic ").append(signature).append("\n"); + } + } + } + } + + result.append("\nDo not use any methods on the classes in the API unless they are explicitly defined above.\n") + .append("Do not include a main function in the class you write. Add all the import statements required.\n"); + + return result.toString(); + } + + public String createLLMFeedbackPrompt(TaskType taskType, GameType gameType, int nPlayers, String className, String code) { + String text = """ + The current best heuristic code is below. + ```java + %s + ``` + Your task is to generate a new heuristic function that is better than the current one. + A better heuristic will have a higher win rate and/or have shorter and less complex code. + You should aim to improve the heuristic function by modifying the existing code, changing relative weights, or adding (or removing) new features. + + """; + String result = String.format(text, code); + String taskText = createLLMTaskPrompt(taskType, gameType, nPlayers, className, true); + return result + taskText; + } + + public String createLLMErrorPrompt(TaskType taskType, GameType gameType, int nPlayers, String className, String code, String error) { + String text = """ + + A previous attempt at this task created the class below. + This class failed to compile correctly. + + %s + + The compilation error message is: + %s + + Your immediate task is to rewrite this code to compile correctly. + """; + String result = String.format(text, code, error); + String taskText = createLLMTaskPrompt(taskType, gameType, nPlayers, className, false); + return taskText + result; + } + + public static class ClassData { + public Class clazz; + public List classMethods; + public String classEnumsAsString; + public String superClass; + public List implementedInterfaces; + } + + public List getAllMethods(Class clazz) { + Map> methods = new HashMap<>(); + Map enums = new HashMap<>(); + ArrayList> clazzez = new ArrayList<>(); + extractMethods(clazz, methods, enums, clazzez); + List retValue = new ArrayList<>(); + for (Class cl : clazzez) { + retValue.add(new ClassData() {{ + clazz = cl; + classMethods = methods.get(cl.getSimpleName()); + classEnumsAsString = enums.get(cl.getSimpleName()); + if (cl.getSuperclass() != null) superClass = cl.getSuperclass().getName(); + implementedInterfaces = getObjectInterfaces(cl); + }}); + } + return retValue; + } + + @SuppressWarnings("unchecked") + private void extractMethods(Class clazz, Map> methods, Map enumValues, List> visitedClasses) { + if (clazz == null || clazz.isPrimitive() || clazz.isArray() || clazz == Object.class || visitedClasses.contains(clazz)) { + return; + } + if (packagesToIgnore.contains(clazz.getPackageName()) && !classesToOverride.contains(clazz.getSimpleName())) { + // System.out.println("Ignoring " + clazz); + return; + } + + System.out.println("Extracting methods from " + clazz); + + visitedClasses.add(clazz); + List methodList = new ArrayList<>(); + Set objectMethodNames = getObjectMethodNames(); + + for (Method method : clazz.getDeclaredMethods()) { + if (!Modifier.isPrivate(method.getModifiers()) && !objectMethodNames.contains(method.getName()) && + (method.getName().startsWith("get") || method.getName().startsWith("is")) && + !method.getName().equals("get") && + !method.getName().contains("String")) { + methodList.add(method); + } + } + if (clazz.isEnum()) { + // We extract a list of the possible enum values + Class> enumClass = (Class>) clazz; + Enum[] enumConstants = enumClass.getEnumConstants(); + StringBuilder values = new StringBuilder(); + for (Enum enumConstant : enumConstants) { + values.append(enumConstant.name()).append(", "); + } + // remove the last comma + values.delete(values.length() - 2, values.length()); + enumValues.put(clazz.getSimpleName(), values.toString()); + } + + methods.put(clazz.getSimpleName(), methodList); + if (!methodList.isEmpty()) { + // We also need to extract classes from the argument lists of the methods + // and from the return types of the methods + + for (Method method : methodList) { + if (method.getReturnType() != Void.TYPE) { + Class returnType = method.getReturnType(); + while (returnType.isArray()) { + returnType = returnType.getComponentType(); + } + // take account of array possibilities + extractMethods(returnType, methods, enumValues, visitedClasses); + + // then we also need to take account of generics + if (method.getGenericReturnType() instanceof ParameterizedType pType) { + for (Type type : pType.getActualTypeArguments()) { + if (type instanceof Class parameterType) { + extractMethods(parameterType, methods, enumValues, visitedClasses); + } + } + } + + } + for (Class parameterType : method.getParameterTypes()) { + extractMethods(parameterType, methods, enumValues, visitedClasses); + } + } + } + + for (Field field : clazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && + !field.getType().isPrimitive()) { + extractMethods(field.getType(), methods, enumValues, visitedClasses); + } + } + + extractMethods(clazz.getSuperclass(), methods, enumValues, visitedClasses); + } + + private Set getObjectMethodNames() { + Set methodNames = new HashSet<>(); + for (Method method : Object.class.getDeclaredMethods()) { + methodNames.add(method.getName()); + } + return methodNames; + } + + private List getObjectInterfaces(Class clazz) { + List interfaces = new ArrayList<>(); + for (Class interf : clazz.getInterfaces()) { + interfaces.add(interf.getName()); + } + return interfaces; + } + + public Map extractJavadocs(File file) { + try { + FileInputStream in = new FileInputStream(file); + CompilationUnit cu = new JavaParser().parse(in).getResult().get(); + Map javadocs = new HashMap<>(); + + cu.findAll(ClassOrInterfaceDeclaration.class).forEach(cls -> { + cls.getJavadoc().ifPresent(javadoc -> javadocs.put(cls.getNameAsString(), javadoc.getDescription().toText())); + cls.findAll(MethodDeclaration.class).forEach(method -> { + String signature = method.getDeclarationAsString(false, false, true); + method.getJavadoc().ifPresent(javadoc -> javadocs.put(signature, javadoc.getDescription().toText())); + }); + }); + + return javadocs; + } catch (Exception e) { + return null; + } + } + + private String getMethodSignature(Method method) { + String returnType = getTypeDescription((method.getGenericReturnType())); + StringBuilder signature = new StringBuilder(returnType); + signature.append(" ").append(method.getName()); + if (method.getTypeParameters().length > 0) { + signature.append("<"); + for (int i = 0; i < method.getTypeParameters().length; i++) { + if (i > 0) { + signature.append(", "); + } + signature.append(method.getTypeParameters()[i].getClass().getSimpleName()); + } + signature.append(">"); + } + + signature.append("("); + Parameter[] params = method.getParameters(); + for (int i = 0; i < params.length; i++) { + if (i > 0) { + signature.append(", "); + } + signature.append(getTypeDescription(params[i].getParameterizedType())); + signature.append(" ").append(params[i].getName()); + } + signature.append(")"); + return signature.toString(); + } + + /** + * Get a description of the type, including generics if present + * However we only return the simple name + generics. + * We don't include the package name. + */ + private String getTypeDescription(Type type) { + if (type instanceof Class clazz) { + return clazz.getSimpleName(); + } + if (type instanceof ParameterizedType pType) { + Type rawType = pType.getRawType(); + StringBuilder result = new StringBuilder(); + if (rawType instanceof Class rawClass) { + result.append(rawClass.getSimpleName()); + } else { + result.append(rawType.getTypeName()); + } + result.append("<"); + for (int i = 0; i < pType.getActualTypeArguments().length; i++) { + if (i > 0) { + result.append(", "); + } + result.append(getTypeDescription(pType.getActualTypeArguments()[i])); + } + result.append(">"); + return result.toString(); + } + return type.getTypeName(); + } + + public static void main(String[] args) { + GamePromptGenerator generator = new GamePromptGenerator(); + System.out.println(generator.createLLMTaskPrompt(TaskType.Heuristic, GameType.Dominion, 2, "TicTacToeHeuristic", false)); + } +} diff --git a/src/main/java/llm/IHasStateHeuristic.java b/src/main/java/llm/IHasStateHeuristic.java new file mode 100644 index 000000000..cae7f0bf8 --- /dev/null +++ b/src/main/java/llm/IHasStateHeuristic.java @@ -0,0 +1,11 @@ +package llm; + +import core.interfaces.IStateHeuristic; + +public interface IHasStateHeuristic { + + void setStateHeuristic(IStateHeuristic heuristic); + + IStateHeuristic getStateHeuristic(); + +} diff --git a/src/main/java/llm/JavaCoder.java b/src/main/java/llm/JavaCoder.java new file mode 100644 index 000000000..b821972c5 --- /dev/null +++ b/src/main/java/llm/JavaCoder.java @@ -0,0 +1,304 @@ +package llm; + +import core.AbstractParameters; +import core.AbstractPlayer; +import dev.langchain4j.model.openai.OpenAiTokenizer; +import evaluation.RunArg; +import evaluation.tournaments.RoundRobinTournament; +import games.GameType; +import llm.LLMAccess.LLM_MODEL; +import llm.LLMAccess.LLM_SIZE; +import players.PlayerFactory; +import players.heuristics.StringHeuristic; +import players.simple.OSLAPlayer; +import players.simple.RandomPlayer; +import utilities.Utils; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.*; + +public class JavaCoder { + + /** + * Args: + * [0]: game name + * dir: working dir + * [2]: evaluator name + * + * @param args + */ + public static void main(String[] args) throws IOException { + + //Arg. Example: dir=llm game=TicTacToe evaluator=TicTacToeEvaluator + // evaluator will default to Evaluator if not provided + String gameName = Utils.getArg(args, "game", "TicTacToe"); + GameType gameType = GameType.valueOf(gameName); + int playerCount = Utils.getArg(args, "players", 2); + String opponent = Utils.getArg(args, "opponent", "random"); + AbstractPlayer opponentPlayer = PlayerFactory.createPlayer(opponent); + String baseAgentLocation = Utils.getArg(args, "baseAgent", opponent); + String workingDir = Utils.getArg(args, "dir", "llm"); + String modelType = Utils.getArg(args, "model", "GEMINI"); + String modelSize = Utils.getArg(args, "size", "SMALL"); + int matchups = Utils.getArg(args, "matchups", 1000); + String matchMode = Utils.getArg(args, "mode", "exhaustive"); + String resultsFile = Utils.getArg(args, "results", workingDir + "/HeuristicSearch_Results.txt"); + String evaluatorName = Utils.getArg(args, "evaluator", gameName + "Evaluator"); + workingDir = workingDir + "/" + modelType + "_" + modelSize + "/" + gameName; + String llmLogFile = workingDir + "/LLM.log"; + String fileStem = workingDir + "/" + evaluatorName; + File results = new File(resultsFile); + boolean headersNeeded = !results.exists(); + + + File dir = new File(workingDir); + if (!dir.exists()) { + dir.mkdirs(); + } + GamePromptGenerator promptGenerator = new GamePromptGenerator(); + + int iteration = 0; + int max_iters = 10; + int currentErrors = 0; + int maxErrorsPerIteration = 3; + + LLM_SIZE llmSize = LLM_SIZE.valueOf(modelSize); + LLM_MODEL llmModel = LLM_MODEL.valueOf(modelType); + + LLMAccess llm = new LLMAccess(llmModel, llmSize, llmLogFile); + List playerList = new ArrayList<>(); + + String generatedCode = ""; + String error = ""; + + double[] scores = new double[max_iters]; + String[] code = new String[max_iters]; + boolean[] safeIterations = new boolean[max_iters]; + + // Stats to gather are: + // - input and output tokens for the model (in LLMAccess) + // - failed calls (compile errors) + // - failed calls (runtime errors) + // Which we want to log once finished (along with the best heuristic) + int compileErrors = 0; + int runtimeErrors = 0; + + while (iteration < max_iters) { + try { + String fileName = fileStem + String.format("%03d.java", iteration); + String className = evaluatorName + String.format("%03d", iteration); + + String llmPrompt = promptGenerator.createLLMTaskPrompt( + GamePromptGenerator.TaskType.Heuristic, + gameType, + playerCount, + className, + true); + + boolean atLeastOneSafePreviousIteration = false; + for (int index = 0; index < iteration; index++) { + if (safeIterations[index]) { + atLeastOneSafePreviousIteration = true; + break; + } + } + if (atLeastOneSafePreviousIteration) { + // find the best score so far, and extract the code for that + String bestCode = ""; + double bestScore = 0.0; + for (int index = 0; index < iteration; index++) { + if (!safeIterations[index]) { + continue; // exclude iterations that failed to compile or threw exceptions + } + if (scores[index] > bestScore) { + bestScore = scores[index]; + bestCode = code[index]; + } + } + + promptGenerator.createLLMFeedbackPrompt( + GamePromptGenerator.TaskType.Heuristic, + gameType, + playerCount, + className, + bestCode); + } + + if (!error.isEmpty()) { + currentErrors++; + llmPrompt = promptGenerator.createLLMErrorPrompt( + GamePromptGenerator.TaskType.Heuristic, + gameType, + playerCount, + className, + generatedCode, + error); + } + + //String.format("This class had failed to compile correctly.%n%n%s%n%nThe error message is %s%n.Rewrite this code to compile correctly%n", generatedCode, error); + error = ""; + // Use regex to extract code between ```java and ``` + // and remove any comments + generatedCode = llm.getResponse(llmPrompt); + //.replaceAll("```java\\s*(.*?)", "$1"); + // .replaceAll("(.*?)```", "$1") + // .replaceAll("//.*\\n", ""); + + String commentPrompt = """ + After the *** is a Java class. + Your task is to remove any comments or JavaDoc from this code. + The output should be the same code, with any syntax corrections, but without any comments. + + The output must include only the final java code. + *** + """; + String commentFreeCode = llm.getResponse(commentPrompt + generatedCode, llmModel, LLM_SIZE.SMALL) + .replaceAll("```java\\s*(.*?)", "$1") + .replaceAll("(.*?)```", "$1"); + writeGeneratedCodeToFile(commentFreeCode, fileName); + code[iteration] = generatedCode; // we store for future prompts (with comments, as these could be useful) + + // TODO: Add an extra call to summarise the functionality of the code (using the version with comments) + // "useful to someone who wanted to write the function anew from a functional specification" + // that might be a good thing to pass to future iterations + + System.out.printf("Iteration %d has generated code%n", iteration); + safeIterations[iteration] = true; + + AbstractPlayer player = PlayerFactory.createPlayer(baseAgentLocation); + if (player instanceof IHasStateHeuristic hPlayer) { + hPlayer.setStateHeuristic(new StringHeuristic(fileName, className)); + } else { + throw new IllegalArgumentException("Agent " + baseAgentLocation + " does not implement IHasStateHeuristic"); + } + + // We now create a StringHeuristic and associated player from the generated code + player.setName(String.format("%s_%03d", player.toString(), iteration)); + playerList.add(player); + + } catch (RuntimeException e) { + System.out.println("Error compiling: " + e.getMessage()); + error = e.getMessage(); + } catch (IOException exception) { + System.out.println("Error writing file: " + exception.getMessage()); + error = exception.getMessage(); + } + + if (!error.isEmpty()) { + // in this case we failed to compile the code, so we don't run the tournament + if (currentErrors >= maxErrorsPerIteration) { + System.out.println("Too many errors, stopping this iteration"); + safeIterations[iteration] = false; + iteration++; + currentErrors = 0; + error = ""; + } else { + System.out.println("Compilation error, re-asking LLM"); + compileErrors++; + } + continue; + } + // set up defaults + Map tournamentConfig = RunArg.parseConfig(new String[]{}, + Collections.singletonList(RunArg.Usage.RunGames)); + // then override the ones we really want + tournamentConfig.putAll(Map.of( + RunArg.game, gameType, + RunArg.matchups, matchups, + RunArg.mode, matchMode, + RunArg.listener, Collections.emptyList(), + RunArg.destDir, workingDir, + RunArg.verbose, false + )); + AbstractParameters params = gameType.createParameters(System.currentTimeMillis()); + + List playersForTournament = new ArrayList<>(playerList); + // we have at least one opponent player for comparison + // and then pad out extra players to the required number for the player count (if needed) + playersForTournament.add(opponentPlayer.copy()); + while (playersForTournament.size() < playerCount) { + playersForTournament.add(opponentPlayer.copy()); + } + + RoundRobinTournament tournament = new RoundRobinTournament( + playersForTournament, gameType, playerCount, params, + tournamentConfig); + try { + tournament.run(); + } catch (Exception | Error e) { + e.printStackTrace(); + System.out.println("Error running up tournament: " + e.getMessage()); + runtimeErrors++; + error = e.getMessage(); + safeIterations[iteration] = false; // exclude the latest heuristic from future consideration + playerList.remove(playerList.size() - 1); // remove the last player from the list + } + if (safeIterations[iteration]) { + // record results if we ran safely + for (int index = 0; index < playerList.size(); index++) { + System.out.printf("Player %s has score %.2f%n", playerList.get(index).toString(), tournament.getWinRate(index)); + } + + // we now extract the scores of the agents, and record these + // the players are added in iteration order, so we can use that + int playerIndex = 0; + for (int i = 0; i <= iteration; i++) { + if (safeIterations[i]) { + scores[i] = tournament.getWinRate(playerIndex); + playerIndex++; + } else { + scores[i] = 0.0; + } + } + } + + iteration++; + currentErrors = 0; + } + + // Final results + System.out.printf("Total Iterations: %d%n", max_iters); + System.out.printf("Compile errors: %d%n", compileErrors); + System.out.printf("Runtime errors: %d%n", runtimeErrors); + System.out.printf("Successful iterations: %d%n", playerList.size()); + // find best score + int bestScoreIndex = -1; + double bestScore = -1.0; + for (int index = 0; index < scores.length; index++) { + if (scores[index] > bestScore) { + bestScore = scores[index]; + bestScoreIndex = index; + } + } + System.out.printf("Best heuristic: %s%n", bestScoreIndex); + try (FileWriter writer = new FileWriter(results, true)) { + if (headersNeeded) { + writer.write("Game, ModelType, ModelSize, Players, Iterations, CompileErrors, RuntimeErrors," + + " InputTokens, OutputTokens, " + + " SuccessfulIterations, BestHeuristic\n"); + } + writer.write(String.format("%s, %s, %s, %d, %d, %d, %d, %d, %d, %d, %d%n", + gameName, modelType, modelSize, playerCount, max_iters, compileErrors, runtimeErrors, + llm.inputTokens, llm.outputTokens, + playerList.size(), bestScoreIndex)); + } + } + + // Write the prompt response to file + public static void writeGeneratedCodeToFile(String generatedCode, String filePath) throws IOException { + List lines = new ArrayList<>(); + for (String line : generatedCode.split("\n")) { + lines.add(line.split("//", 1)[0].strip() + "\n"); + } + + try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) { + for (String line : lines) { + writer.write(line); + } + } + } + +} diff --git a/src/main/java/llm/JavaCoderAllAgents.java b/src/main/java/llm/JavaCoderAllAgents.java new file mode 100644 index 000000000..59c46c069 --- /dev/null +++ b/src/main/java/llm/JavaCoderAllAgents.java @@ -0,0 +1,25 @@ +package llm; + +import java.io.IOException; + +public class JavaCoderAllAgents { + + public static void main(String[] args) throws IOException { + // we run JavaCoder for the specified args with each model type and size + + for (LLMAccess.LLM_SIZE size : LLMAccess.LLM_SIZE.values()) { + // LLMAccess.LLM_SIZE size = LLMAccess.LLM_SIZE.LARGE; + for (LLMAccess.LLM_MODEL model : LLMAccess.LLM_MODEL.values()) { + // LLMAccess.LLM_MODEL model = LLMAccess.LLM_MODEL.ANTHROPIC; + if (model == LLMAccess.LLM_MODEL.LLAMA) { + continue; + } + String[] newArgs = new String[args.length + 2]; + System.arraycopy(args, 0, newArgs, 0, args.length); + newArgs[args.length] = "model=" + model.toString(); + newArgs[args.length + 1] = "size=" + size.toString(); + JavaCoder.main(newArgs); + } + } + } +} diff --git a/src/main/java/llm/LLMAccess.java b/src/main/java/llm/LLMAccess.java new file mode 100644 index 000000000..cfd11641d --- /dev/null +++ b/src/main/java/llm/LLMAccess.java @@ -0,0 +1,212 @@ +package llm; + +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.anthropic.AnthropicChatModelName; +import dev.langchain4j.model.chat.ChatLanguageModel; +import dev.langchain4j.model.mistralai.MistralAiChatModelName; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.openai.OpenAiChatModelName; +import dev.langchain4j.model.openai.OpenAiTokenizer; +import dev.langchain4j.model.vertexai.VertexAiGeminiChatModel; +import dev.langchain4j.model.mistralai.MistralAiChatModel; + +import java.io.File; +import java.io.FileWriter; + +public class LLMAccess { + + static ChatLanguageModel[] geminiModel = new ChatLanguageModel[2]; + static ChatLanguageModel[] mistralModel = new ChatLanguageModel[2]; + static ChatLanguageModel[] openaiModel = new ChatLanguageModel[2]; + static ChatLanguageModel[] anthropicModel = new ChatLanguageModel[2]; + static ChatLanguageModel[] llamaModel = new ChatLanguageModel[2]; + + static OpenAiTokenizer tokenizer = new OpenAiTokenizer(); + + String mistralToken = System.getenv("MISTRAL_TOKEN"); + String geminiProject = System.getenv("GEMINI_PROJECT"); + String openaiToken = System.getenv("OPENAI_TOKEN"); + String anthropicToken = System.getenv("ANTHROPIC_TOKEN"); + + File logFile; + FileWriter logWriter; + + String geminiLocation = "europe-west2"; + String llamaLocation = "us-central1"; + + LLM_MODEL modelType; + LLM_SIZE modelSize; + + long inputTokens = 0; + long outputTokens = 0; + + public enum LLM_MODEL { + GEMINI, + MISTRAL, + OPENAI, + ANTHROPIC, + LLAMA + } + + public enum LLM_SIZE { + SMALL, + LARGE + } + + + /** + * Constructor for LLMAccess + * + * @param modelType - the model to use as the default (this can be overridden in the getResponse method) + * @param modelSize - the size of the model to use (this can be overridden in the getResponse method) + * @param logFileName - the name of the log file to write to. this will include all the messages sent to any LLM + */ + public LLMAccess(LLM_MODEL modelType, LLM_SIZE modelSize, String logFileName) { + if (logFileName != null && !logFileName.isEmpty()) { + logFile = new File(logFileName); + try { + logWriter = new FileWriter(logFile); + } catch (Exception e) { + System.out.println("Error creating log file: " + e.getMessage()); + } + } + this.modelType = modelType; + this.modelSize = modelSize; + if (geminiProject != null && !geminiProject.isEmpty()) { + try { + geminiModel[1] = VertexAiGeminiChatModel.builder() + .project(geminiProject) + .location(geminiLocation) + // .temperature(1.0f) // between 0 and 2; default 1.0 for pro-1.5 + // .topK(40) // some models have a three-stage sampling process. topK; then topP; then temperature + // .topP(0.94f) // 1.5 default is 0.64; the is the sum of probability of tokens to sample from + // .maxOutputTokens(1000) // max replay size (max is 8192) + // .modelName("gemini-1.5-pro") // $1.25 per million characters input, $0.3125 per million output + .modelName("gemini-1.5-pro") // $0.075 per million characters output, $0.01875 per million characters input + .build(); + geminiModel[0] = VertexAiGeminiChatModel.builder() + .project(geminiProject) + .location(geminiLocation) + .modelName("gemini-1.5-flash") + .build(); + } catch (Error e) { + System.out.println("Error creating Gemini model: " + e.getMessage()); + } + + + try { + llamaModel[1] = VertexAiGeminiChatModel.builder() + .project(geminiProject) + .location(llamaLocation) + // .temperature(1.0f) // between 0 and 2; default 1.0 for pro-1.5 + // .topK(40) // some models have a three-stage sampling process. topK; then topP; then temperature + // .topP(0.94f) // 1.5 default is 0.64; the is the sum of probability of tokens to sample from + // .maxOutputTokens(1000) // max replay size (max is 8192) + // .modelName("gemini-1.5-pro") // $1.25 per million characters input, $0.3125 per million output + .modelName("llama3-405b-instruct-maas") // $0.075 per million characters output, $0.01875 per million characters input + .build(); + llamaModel[0] = VertexAiGeminiChatModel.builder() + .project(geminiProject) + .location(llamaLocation) + .modelName("llama3-70b-instruct-maas") + .build(); + } catch (Error e) { + System.out.println("Error creating Llama model: " + e.getMessage()); + } + } + + if (mistralToken != null && !mistralToken.isEmpty()) { + mistralModel[0] = MistralAiChatModel.builder() + .modelName(MistralAiChatModelName.MISTRAL_SMALL_LATEST) + .apiKey(mistralToken) + .build(); + mistralModel[1] = MistralAiChatModel.builder() + .modelName(MistralAiChatModelName.MISTRAL_LARGE_LATEST) + .apiKey(mistralToken) + .build(); + // $2 per million input tokens, $6 per million output tokens + } + + if (openaiToken != null && !openaiToken.isEmpty()) { + openaiModel[0] = OpenAiChatModel.builder() + .modelName(OpenAiChatModelName.GPT_4_O_MINI) // $0.15 per million input tokens, $0.6 per million output tokens + .apiKey(openaiToken) + .build(); + openaiModel[1] = OpenAiChatModel.builder() + .modelName(OpenAiChatModelName.GPT_4_O) // $5 per million input tokens, $15 per million output tokens + .apiKey(openaiToken) + .build(); + } + + if (anthropicToken != null && !anthropicToken.isEmpty()) { + anthropicModel[0] = AnthropicChatModel.builder() + .modelName(AnthropicChatModelName.CLAUDE_3_HAIKU_20240307) // $0.25 per million input tokens, $1.25 per million output tokens + .apiKey(anthropicToken) + .maxTokens(4096) + .build(); + anthropicModel[1] = AnthropicChatModel.builder() + .modelName(AnthropicChatModelName.CLAUDE_3_5_SONNET_20240620) // $3 per million input tokens, $15 per million output tokens + .apiKey(anthropicToken) + .maxTokens(8192) + .build(); + } + + } + + /** + * Gets some text from the specified model + * + * @param query the prompt sent to the model + * @param modelType the LLM model to use + * @return The full text returned by the model; or an empty string if no valid model + */ + public String getResponse(String query, LLM_MODEL modelType, LLM_SIZE modelSize) { + String response = ""; + ChatLanguageModel modelToUse = switch(modelType) { + case MISTRAL -> modelSize == LLM_SIZE.SMALL ? mistralModel[0] : mistralModel[1]; + case GEMINI -> modelSize == LLM_SIZE.SMALL ? geminiModel[0] : geminiModel[1]; + case OPENAI -> modelSize == LLM_SIZE.SMALL ? openaiModel[0] : openaiModel[1]; + case ANTHROPIC -> modelSize == LLM_SIZE.SMALL ? anthropicModel[0] : anthropicModel[1]; + case LLAMA -> modelSize == LLM_SIZE.SMALL ? llamaModel[0] : llamaModel[1]; + }; + if (modelToUse != null) { + try { + inputTokens += tokenizer.estimateTokenCountInText(query); + response = modelToUse.generate(query); + outputTokens += tokenizer.estimateTokenCountInText(response); + } catch (Exception e) { + System.out.println("Error getting response from model: " + e.getMessage()); + } + } else { + System.out.println("No valid model available for " + modelType + " " + modelSize); + return "No reply available"; + } + // Write to file (if log file is specified) + if (logWriter != null) { + String output = String.format("\nModel: %s\nQuery: %s\nResponse: %s\n", modelType, query, response); + try { + logWriter.write(output); + logWriter.flush(); + } catch (Exception e) { + System.out.println("Error writing to log file: " + e.getMessage()); + } + } + return response; + } + + /** + * This will use the default LLM model specified in the constructor + * + * @param query the prompt sent to the model + * @return The full text returned by the model; or an empty string if no valid model + */ + public String getResponse(String query) { + return getResponse(query, this.modelType, this.modelSize); + } + + public static void main(String[] args) { + LLMAccess llm = new LLMAccess(LLM_MODEL.OPENAI, LLM_SIZE.LARGE, "llm_log.txt"); + llm.getResponse("What is the average lifespan of a Spanish Armadillo?"); + llm.getResponse("What is the lifecycle of the European Firefly?", LLM_MODEL.OPENAI, LLM_SIZE.SMALL); + } +} diff --git a/src/main/java/llm/StringHeuristicTournament.java b/src/main/java/llm/StringHeuristicTournament.java new file mode 100644 index 000000000..22c453889 --- /dev/null +++ b/src/main/java/llm/StringHeuristicTournament.java @@ -0,0 +1,86 @@ +package llm; + +import core.AbstractPlayer; +import evaluation.RunArg; +import evaluation.listeners.IGameListener; +import evaluation.tournaments.RoundRobinTournament; +import games.GameType; +import players.PlayerFactory; +import players.heuristics.StringHeuristic; +import players.search.MaxNSearchPlayer; +import utilities.Utils; + +import java.io.*; +import java.util.*; +import java.util.regex.Pattern; + +import static evaluation.RunArg.*; + +public class StringHeuristicTournament { + + public static void main(String[] args) { + + // We take in all the configuration parameters suitable for RunGames + // and then we also need a config file with the StringHeuristic code details + // Being a list of .java file, each of which needs to be read in, compiled, and + // used in initialise a StringHeuristic player. + + String agentConfigFile = Utils.getArg(args, "rawJavaDetails", "RawJavaDetails.txt"); + // Then read this file, with each row having two fields (comma separated) + // i) the name of the agent + // ii) the path to the .java file + + File file = new File(agentConfigFile); + if (!file.exists()) { + System.out.println("File not found: " + agentConfigFile); + return; + } + String agentConfig = Utils.getArg(args, "playerDirectory", "osla"); + + GameType gameType = GameType.valueOf(Utils.getArg(args, "game", "TicTacToe")); + int nPlayers = Integer.parseInt(Utils.getArg(args, "nPlayers", "2")); + + + List players = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + String[] parts = line.split(","); + if (parts.length != 2) { + System.out.println("Invalid line: " + line); + continue; + } + String agentName = parts[0].trim(); + String className = agentName + "_" + gameType.name(); + String fileName = parts[1].trim(); + // String className = fileName.replaceAll(".*(/|\\\\)(.*?)\\.java", "$2"); + StringHeuristic heuristic = new StringHeuristic(fileName, className); + + // To get the player from the StringHeuristic we rely on implementation of IHasStateHeuristic + AbstractPlayer player = PlayerFactory.createPlayer(agentConfig); + if (player instanceof IHasStateHeuristic playerWithHeuristic) { + playerWithHeuristic.setStateHeuristic(heuristic); + } else { + throw new IllegalArgumentException("Player does not support IHasStateHeuristic: " + player.getClass()); + } + player.setName(agentName); + players.add(player); + } + + Map config = parseConfig(args, Collections.singletonList(RunArg.Usage.RunGames)); + + RoundRobinTournament tournament = new RoundRobinTournament(players, gameType, nPlayers, null, config); + //noinspection unchecked + for (String listenerClass : ((List) config.get(listener))) { + IGameListener gameTracker = IGameListener.createListener(listenerClass); + tournament.addListener(gameTracker); + String outputDir = (String) config.get(destDir); + gameTracker.setOutputDirectory(outputDir); + } + + tournament.run(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/players/heuristics/StateHeuristicType.java b/src/main/java/players/heuristics/StateHeuristicType.java new file mode 100644 index 000000000..a911e41a2 --- /dev/null +++ b/src/main/java/players/heuristics/StateHeuristicType.java @@ -0,0 +1,28 @@ +package players.heuristics; + +import core.interfaces.IStateHeuristic; + +import javax.swing.plaf.nimbus.State; + +public enum StateHeuristicType { + StringHeuristic(), + GameSpecificHeuristic(new GameDefaultHeuristic()), + WinOnlyHeuristic(new WinOnlyHeuristic()), + OrdinalPosition(new OrdinalPosition()), + WinPlusHeuristic(new WinPlusHeuristic(1000.0)), + LeaderHeuristic(new LeaderHeuristic()), + PureScoreHeuristic(new PureScoreHeuristic()), + ScoreHeuristic(new ScoreHeuristic()), + NullHeuristic(new NullHeuristic()); + + final IStateHeuristic heuristicFunc; + + StateHeuristicType() { + this.heuristicFunc = null; + } + + StateHeuristicType(IStateHeuristic heuristicFunc) { + this.heuristicFunc = heuristicFunc; + } + +} diff --git a/src/main/java/players/heuristics/StringHeuristic.java b/src/main/java/players/heuristics/StringHeuristic.java new file mode 100644 index 000000000..797b459e8 --- /dev/null +++ b/src/main/java/players/heuristics/StringHeuristic.java @@ -0,0 +1,152 @@ +package players.heuristics; + +import com.fasterxml.jackson.databind.annotation.NoClass; +import core.AbstractGameState; +import core.CoreConstants; +import core.interfaces.IStateHeuristic; +import games.loveletter.LoveLetterGameState; +import games.tictactoe.TicTacToeGameState; + +import javax.tools.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; + +public class StringHeuristic implements IStateHeuristic { + + private final String className; + private final String fileName; + + private String str; + + Object heuristicClass; + Method heuristicFunction; + + + public String getFileName() { + return fileName; + } + + public String getHeuristicCode() { + return str; + } + + public void setHeuristicCode(String s) { + this.str = s; + compile(); + } + + public StringHeuristic(String fileName, String className) { + this.fileName = fileName; + this.className = className; + loadFile(); + compile(); + } + + private void loadFile() { + // Read 'str' as whole text in fileName file: + try { + BufferedReader reader = new BufferedReader(new FileReader(fileName)); + StringBuilder stringBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuilder.append(line).append("\n"); + } + str = stringBuilder.toString(); + reader.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void compile() { + // Method string + //String className = fileName.replaceAll(".*/(.*?)\\.java", "$1"); + // Replace class name in the source code + String sourceCode = str.replaceAll("public class .*? \\{", "public class " + className + " {"); + + // Compile source code + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + + // Create a file object for the source code + JavaFileObject javaFileObject = new SimpleJavaFileObject( + URI.create("string:///" + className + ".java"), JavaFileObject.Kind.SOURCE) { + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return sourceCode; + } + }; + + // Compile the source code + DiagnosticCollector diagnosticsCollector = new DiagnosticCollector(); + + JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, + diagnosticsCollector, null, null, + List.of(javaFileObject)); + + boolean success = task.call(); + if (!success) { + StringBuilder sb = new StringBuilder(); + List> diagnostics = diagnosticsCollector.getDiagnostics(); + for (Diagnostic diagnostic : diagnostics) { + if (diagnostic.getKind() != Diagnostic.Kind.NOTE) { + // read error details from the diagnostic object + sb.append(diagnostic.getMessage(null)).append("\n"); + } + } + String error = String.format("Compilation error: %s", sb); + throw new RuntimeException(error); + } else { + System.out.println("Heuristic loaded: " + fileName); + } + + // Load the compiled class + try { + URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI().toURL()}); + + Class dynamicClass = classLoader.loadClass(className); + + // Create an instance of the compiled class + heuristicClass = dynamicClass.getDeclaredConstructor().newInstance(); + + // Find and invoke the method using reflection + heuristicFunction = dynamicClass.getMethod("evaluateState", AbstractGameState.class, int.class); + + classLoader.close(); + } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException | IOException | NoClassDefFoundError e) { + throw new RuntimeException(e); + } + } + + @Override + public double evaluateState(AbstractGameState gs, int playerId) { + CoreConstants.GameResult playerResult = gs.getPlayerResults()[playerId]; + if (playerResult == CoreConstants.GameResult.LOSE_GAME) + return 0; + if (playerResult == CoreConstants.GameResult.DRAW_GAME) + return 0.5; + if (playerResult == CoreConstants.GameResult.WIN_GAME) + return 1; + + try { + return (double) heuristicFunction.invoke(heuristicClass, gs, playerId); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Error invoking heuristic function as it returns a null value : ", e); + } + } + + @Override + public String toString() { + return "StringHeuristic: " + fileName; + } +} diff --git a/src/main/java/players/mcts/MCTSPlayer.java b/src/main/java/players/mcts/MCTSPlayer.java index 522ce2818..c35adc708 100644 --- a/src/main/java/players/mcts/MCTSPlayer.java +++ b/src/main/java/players/mcts/MCTSPlayer.java @@ -8,6 +8,7 @@ import evaluation.listeners.IGameListener; import core.interfaces.IStateHeuristic; import evaluation.metrics.Event; +import llm.IHasStateHeuristic; import players.IAnyTimePlayer; import utilities.Pair; import utilities.Utils; @@ -21,7 +22,7 @@ import static players.mcts.MCTSEnums.OpponentTreePolicy.*; import static players.mcts.MCTSEnums.OpponentTreePolicy.MultiTree; -public class MCTSPlayer extends AbstractPlayer implements IAnyTimePlayer { +public class MCTSPlayer extends AbstractPlayer implements IAnyTimePlayer, IHasStateHeuristic { // Heuristics used for the agent protected boolean debug = false; @@ -360,6 +361,15 @@ public int getBudget() { return parameters.budget; } + @Override + public void setStateHeuristic(IStateHeuristic heuristic) { + getParameters().setParameterValue("heuristic", heuristic); + } + @Override + public IStateHeuristic getStateHeuristic() { + return getParameters().heuristic; + } + @Override public String toString() { return super.toString(); diff --git a/src/main/java/players/mcts/SingleTreeNode.java b/src/main/java/players/mcts/SingleTreeNode.java index 6a85cfa82..38fabe381 100644 --- a/src/main/java/players/mcts/SingleTreeNode.java +++ b/src/main/java/players/mcts/SingleTreeNode.java @@ -915,8 +915,8 @@ protected double[] rollout(int lastActor) { for (int i = 0; i < retValue.length; i++) { retValue[i] = params.heuristic.evaluateState(rolloutState, i); - if (Double.isNaN(retValue[i])) - throw new AssertionError("Illegal heuristic value - should be a number"); + if (Double.isNaN(retValue[i]) || Double.isInfinite(retValue[i])) + throw new AssertionError("Illegal heuristic value - should be a number - " + params.heuristic.toString()); } return retValue; } @@ -973,6 +973,8 @@ protected void normaliseRewardsAfterIteration(double[] result) { if (root.highReward < stats.getMax()) root.highReward = stats.getMax(); } + if (root.lowReward == Double.NEGATIVE_INFINITY || root.highReward == Double.POSITIVE_INFINITY) + throw new AssertionError("We have somehow failed to update the min or max rewards"); } protected double[] processResultsForParanoidOrSelfOnly(double[] result) { diff --git a/src/main/java/players/search/MaxNSearchParameters.java b/src/main/java/players/search/MaxNSearchParameters.java index f035a9506..832a38416 100644 --- a/src/main/java/players/search/MaxNSearchParameters.java +++ b/src/main/java/players/search/MaxNSearchParameters.java @@ -34,7 +34,7 @@ public void _reset() { heuristic = new GameDefaultHeuristic(); } if (budgetType != PlayerConstants.BUDGET_TIME) { - System.out.println("Warning: SearchPlayer only supports time-based budget limits. Setting to BUDGET_TIME."); + // System.out.println("Warning: SearchPlayer only supports time-based budget limits. Setting to BUDGET_TIME."); budgetType = PlayerConstants.BUDGET_TIME; } } diff --git a/src/main/java/players/search/MaxNSearchPlayer.java b/src/main/java/players/search/MaxNSearchPlayer.java index 869021f48..85336dbc0 100644 --- a/src/main/java/players/search/MaxNSearchPlayer.java +++ b/src/main/java/players/search/MaxNSearchPlayer.java @@ -2,11 +2,13 @@ import core.*; import core.actions.AbstractAction; +import core.interfaces.IStateHeuristic; +import llm.IHasStateHeuristic; import java.util.Collections; import java.util.List; -public class MaxNSearchPlayer extends AbstractPlayer { +public class MaxNSearchPlayer extends AbstractPlayer implements IHasStateHeuristic { /** * This class is a simple implementation of a MaxN search player. * (This is the same as Minimax in the case of 2 players if the heuristic is symmetric) @@ -26,13 +28,14 @@ public class MaxNSearchPlayer extends AbstractPlayer { * - ACTION: D is decremented at each decision node * - MACRO_ACTION: D is decremented at each decision node where the acting player changes * - TURN: D is decremented at each decision node where the turn number changes - * + *

* Additionally, the BUDGET can be specified as a cutoff for the search. If this much time passes * without the search finishing, the best action found so far is returned (likely to be pretty random). */ private long startTime; + public MaxNSearchPlayer(MaxNSearchParameters parameters) { super(parameters, "MinMaxSearch"); } @@ -53,6 +56,16 @@ public AbstractAction _getAction(AbstractGameState gs, List acti return expand(gs, actions, getParameters().searchDepth).action; } + @Override + public void setStateHeuristic(IStateHeuristic heuristic) { + getParameters().setParameterValue("heuristic", heuristic); + } + + @Override + public IStateHeuristic getStateHeuristic() { + return getParameters().heuristic; + } + /** * This returns a Pair. * The first element is the best action to take, based on the recursive search. @@ -107,7 +120,7 @@ protected SearchResult expand(AbstractGameState state, List acti SearchResult result = expand(stateCopy, nextActions, newDepth); // we make the decision based on the actor at state, not the actor at stateCopy - if (result.value[state.getCurrentPlayer()] > bestValue) { + if (result.value[state.getCurrentPlayer()] > bestValue) { bestAction = action; bestValues = result.value; bestValue = bestValues[state.getCurrentPlayer()]; @@ -122,7 +135,8 @@ protected SearchResult expand(AbstractGameState state, List acti @Override public MaxNSearchPlayer copy() { MaxNSearchPlayer retValue = new MaxNSearchPlayer((MaxNSearchParameters) getParameters().shallowCopy()); - retValue.setForwardModel(getForwardModel().copy()); + if (getForwardModel() != null) + retValue.setForwardModel(getForwardModel().copy()); return retValue; } diff --git a/src/main/java/players/simple/OSLAPlayer.java b/src/main/java/players/simple/OSLAPlayer.java index 9c3ad83fd..453efb65d 100644 --- a/src/main/java/players/simple/OSLAPlayer.java +++ b/src/main/java/players/simple/OSLAPlayer.java @@ -54,7 +54,7 @@ public AbstractAction _getAction(AbstractGameState gs, List acti double Q = noise(valState[actionIndex], getParameters().noiseEpsilon, rnd.nextDouble()); - if (Q > maxQ) { + if (Q > maxQ || bestAction == null) { maxQ = Q; bestAction = action; } diff --git a/src/test/java/games/dominion/BaseActionCardsTest.java b/src/test/java/games/dominion/BaseActionCardsTest.java index d761164b8..14f847804 100644 --- a/src/test/java/games/dominion/BaseActionCardsTest.java +++ b/src/test/java/games/dominion/BaseActionCardsTest.java @@ -41,11 +41,11 @@ public void village() { DominionAction village = new SimpleAction(CardType.VILLAGE, 0); state.addCard(CardType.VILLAGE, 0, DeckType.HAND); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); fm.next(state, village); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(2, state.actionsLeft()); + assertEquals(2, state.getActionsLeft()); } @Test @@ -54,11 +54,11 @@ public void smithy() { DominionAction smithy = new SimpleAction(CardType.SMITHY, 0); state.addCard(CardType.SMITHY, 0, DeckType.HAND); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); fm.next(state, smithy); assertEquals(8, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); } @Test @@ -67,11 +67,11 @@ public void laboratory() { DominionAction laboratory = new SimpleAction(CardType.LABORATORY, 0); state.addCard(CardType.LABORATORY, 0, DeckType.HAND); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); fm.next(state, laboratory); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); } @Test @@ -80,15 +80,15 @@ public void market() { DominionAction market = new SimpleAction(CardType.MARKET, 0); state.addCard(CardType.MARKET, 0, DeckType.HAND); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getActionsLeft()); + assertEquals(1, state.getBuysLeft()); fm.next(state, market); assertEquals(Play, state.getGamePhase()); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); int money = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); - assertEquals(money + 1, state.availableSpend(0)); + assertEquals(money + 1, state.getAvailableSpend(0)); } @Test @@ -97,15 +97,15 @@ public void festival() { DominionAction festival = new SimpleAction(CardType.FESTIVAL, 0); state.addCard(CardType.FESTIVAL, 0, DeckType.HAND); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); - assertEquals(1, state.buysLeft()); - int money = state.availableSpend(0); + assertEquals(1, state.getActionsLeft()); + assertEquals(1, state.getBuysLeft()); + int money = state.getAvailableSpend(0); fm.next(state, festival); assertEquals(Play, state.getGamePhase()); assertEquals(5, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(2, state.actionsLeft()); - assertEquals(money + 2, state.availableSpend(0)); + assertEquals(2, state.getActionsLeft()); + assertEquals(money + 2, state.getAvailableSpend(0)); } @Test @@ -119,7 +119,7 @@ public void cellarBase() { assertEquals(state.currentActionInProgress(), cellar); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); List cellarActions = fm.computeAvailableActions(state); assertEquals(3, cellarActions.size()); @@ -165,10 +165,10 @@ public void militiaCausesAllOtherPlayersToDiscardDownToFive() { for (int i = 0; i < 4; i++) { if (i != 2) assertEquals(5, state.getDeck(DeckType.HAND, i).getSize()); } - int start = state.availableSpend(2); + int start = state.getAvailableSpend(2); fm.next(state, militia); assertEquals(3, state.getCurrentPlayer()); - assertEquals(start + 2, state.availableSpend(2)); + assertEquals(start + 2, state.getAvailableSpend(2)); do { List actionsAvailable = fm.computeAvailableActions(state); assertTrue(actionsAvailable.stream().allMatch(a -> a instanceof DiscardCard)); @@ -238,7 +238,7 @@ public void moat() { fm.next(state, moat); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(1, state.getDeck(DeckType.TABLE, 0).getSize()); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertFalse(state.isDefended(0)); } @@ -411,13 +411,13 @@ public void remodelBuyOptionsCorrectGivenTrashedCard() { state.addCard(CardType.ESTATE, 0, DeckType.HAND); Remodel remodel = new Remodel(0); fm.next(state, remodel); - int availableSpend = state.availableSpend(0); + int availableSpend = state.getAvailableSpend(0); fm.next(state, new TrashCard(CardType.ESTATE, 0)); List actions = fm.computeAvailableActions(state); assertTrue(actions.stream().allMatch(a -> a instanceof GainCard)); assertTrue(actions.stream().allMatch(a -> ((GainCard) a).cardType.cost <= 4)); - List allCards = state.cardsToBuy(); + List allCards = state.getCardsToBuy(); List allGainable = actions.stream().map(a -> ((GainCard) a).cardType).collect(toList()); allCards.removeAll(allGainable); assertTrue(allCards.stream().allMatch(c -> c.cost >= 5)); @@ -425,7 +425,7 @@ public void remodelBuyOptionsCorrectGivenTrashedCard() { fm.next(state, new GainCard(CardType.SILVER, 0)); assertEquals(0, state.getCurrentPlayer()); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(availableSpend, state.availableSpend(0)); + assertEquals(availableSpend, state.getAvailableSpend(0)); } @Test @@ -434,13 +434,13 @@ public void remodelPossibleWithNoBuyableCards() { state.addCard(CardType.REMODEL, 0, DeckType.HAND); state.addCard(CardType.ESTATE, 0, DeckType.HAND); // now remove all cost 2 cards - while (state.cardsToBuy().contains(CardType.ESTATE)) + while (state.getCardsToBuy().contains(CardType.ESTATE)) state.removeCardFromTable(CardType.ESTATE); - while (state.cardsToBuy().contains(CardType.COPPER)) + while (state.getCardsToBuy().contains(CardType.COPPER)) state.removeCardFromTable(CardType.COPPER); - while (state.cardsToBuy().contains(CardType.MOAT)) + while (state.getCardsToBuy().contains(CardType.MOAT)) state.removeCardFromTable(CardType.MOAT); - while (state.cardsToBuy().contains(CardType.CELLAR)) + while (state.getCardsToBuy().contains(CardType.CELLAR)) state.removeCardFromTable(CardType.CELLAR); Remodel remodel = new Remodel(0); fm.next(state, remodel); @@ -474,13 +474,13 @@ public void merchantWithNoSilverInHand() { int treasureValue = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); assertEquals(Play, state.getGamePhase()); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); - assertEquals(treasureValue, state.availableSpend(0)); + assertEquals(1, state.getActionsLeft()); + assertEquals(treasureValue, state.getAvailableSpend(0)); fm.next(state, new EndPhase(Play)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(treasureValue, state.availableSpend(0)); - assertEquals(1, state.buysLeft()); + assertEquals(treasureValue, state.getAvailableSpend(0)); + assertEquals(1, state.getBuysLeft()); } @Test @@ -494,13 +494,13 @@ public void merchantWithOneSilver() { int treasureValue = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); assertEquals(Play, state.getGamePhase()); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); - assertEquals(treasureValue, state.availableSpend(0)); + assertEquals(1, state.getActionsLeft()); + assertEquals(treasureValue, state.getAvailableSpend(0)); fm.next(state, new EndPhase(Play)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(treasureValue + 1, state.availableSpend(0)); - assertEquals(1, state.buysLeft()); + assertEquals(treasureValue + 1, state.getAvailableSpend(0)); + assertEquals(1, state.getBuysLeft()); } @Test @@ -515,13 +515,13 @@ public void merchantWithTwoSilver() { int treasureValue = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); assertEquals(Play, state.getGamePhase()); assertEquals(8, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); - assertEquals(treasureValue, state.availableSpend(0)); + assertEquals(1, state.getActionsLeft()); + assertEquals(treasureValue, state.getAvailableSpend(0)); fm.next(state, new EndPhase(Play)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(treasureValue + 1, state.availableSpend(0)); - assertEquals(1, state.buysLeft()); + assertEquals(treasureValue + 1, state.getAvailableSpend(0)); + assertEquals(1, state.getBuysLeft()); } @Test @@ -538,13 +538,13 @@ public void merchantsWithTwoSilver() { int treasureValue = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); assertEquals(Play, state.getGamePhase()); assertEquals(9, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); - assertEquals(treasureValue, state.availableSpend(0)); + assertEquals(1, state.getActionsLeft()); + assertEquals(treasureValue, state.getAvailableSpend(0)); fm.next(state, new EndPhase(Play)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(treasureValue + 2, state.availableSpend(0)); - assertEquals(1, state.buysLeft()); + assertEquals(treasureValue + 2, state.getAvailableSpend(0)); + assertEquals(1, state.getBuysLeft()); } @Test @@ -553,9 +553,9 @@ public void workshop() { state.addCard(CardType.WORKSHOP, 0, DeckType.HAND); Workshop workshop = new Workshop(0); - int startSpend = state.availableSpend(0); + int startSpend = state.getAvailableSpend(0); fm.next(state, workshop); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertEquals(0, state.getCurrentPlayer()); assertFalse(workshop.executionComplete(state)); List availableActions = fm.computeAvailableActions(state); @@ -568,11 +568,11 @@ public void workshop() { } ); fm.next(state, availableActions.get(3)); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertEquals(0, state.getCurrentPlayer()); assertTrue(workshop.executionComplete(state)); - assertEquals(startSpend, state.availableSpend(0)); - assertEquals(1, state.buysLeft()); + assertEquals(startSpend, state.getAvailableSpend(0)); + assertEquals(1, state.getBuysLeft()); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); } @@ -583,9 +583,9 @@ public void mine() { state.addCard(CardType.SILVER, 0, DeckType.HAND); Mine mine = new Mine(0); - int startSpend = state.availableSpend(0); + int startSpend = state.getAvailableSpend(0); fm.next(state, mine); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertEquals(0, state.getCurrentPlayer()); assertFalse(mine.executionComplete(state)); assertEquals(Play, state.getGamePhase()); @@ -605,7 +605,7 @@ public void mine() { assertTrue(availableActions.contains(new GainCard(CardType.GOLD, 0, DeckType.HAND))); fm.next(state, new GainCard(CardType.GOLD, 0, DeckType.HAND)); - assertEquals(startSpend + 1, state.availableSpend(0)); + assertEquals(startSpend + 1, state.getAvailableSpend(0)); assertTrue(mine.executionComplete(state)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); } @@ -621,12 +621,12 @@ public void mineWithNoTreasure() { Mine mine = new Mine(0); fm.next(state, mine); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertEquals(0, state.getCurrentPlayer()); assertTrue(mine.executionComplete(state)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); assertEquals(0, state.getCurrentPlayer()); - assertEquals(0, state.availableSpend(0)); + assertEquals(0, state.getAvailableSpend(0)); } @Test @@ -684,15 +684,15 @@ public void moneylender() { DominionGameState state = (DominionGameState) game.getGameState(); state.addCard(CardType.MONEYLENDER, 0, DeckType.HAND); Moneylender moneylender = new Moneylender(0); - int startSpend = state.availableSpend(0); + int startSpend = state.getAvailableSpend(0); long copperInHand = state.getDeck(DeckType.HAND, 0).stream() .filter(c -> c.cardType() == CardType.COPPER).count(); fm.next(state, moneylender); - assertEquals(startSpend + 2, state.availableSpend(0)); + assertEquals(startSpend + 2, state.getAvailableSpend(0)); assertEquals(copperInHand - 1L, state.getDeck(DeckType.HAND, 0).stream() .filter(c -> c.cardType() == CardType.COPPER).count()); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getBuysLeft()); assertEquals(CardType.COPPER, state.getDeck(DeckType.TRASH, -1).get(0).cardType()); } @@ -707,9 +707,9 @@ public void moneylenderWithoutCopper() { Moneylender moneylender = new Moneylender(0); fm.next(state, moneylender); - assertEquals(2, state.availableSpend(0)); + assertEquals(2, state.getAvailableSpend(0)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getBuysLeft()); } @Test @@ -718,15 +718,15 @@ public void poacherWithNoEmptyPiles() { state.addCard(CardType.POACHER, 0, DeckType.HAND); state.addCard(CardType.ESTATE, 0, DeckType.DRAW); Poacher poacher = new Poacher(0); - int startSpend = state.availableSpend(0); + int startSpend = state.getAvailableSpend(0); fm.next(state, poacher); - assertEquals(startSpend + 1, state.availableSpend(0)); - assertEquals(1, state.actionsLeft()); + assertEquals(startSpend + 1, state.getAvailableSpend(0)); + assertEquals(1, state.getActionsLeft()); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(5, state.getDeck(DeckType.DRAW, 0).getSize()); assertEquals(Play, state.getGamePhase()); - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getBuysLeft()); assertFalse(state.isActionInProgress()); } @@ -743,12 +743,12 @@ public void poacherWithTwoEmptyPiles() { fm.next(state, poacher); int startSpend = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); - assertEquals(startSpend + 1, state.availableSpend(0)); - assertEquals(1, state.actionsLeft()); + assertEquals(startSpend + 1, state.getAvailableSpend(0)); + assertEquals(1, state.getActionsLeft()); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(4, state.getDeck(DeckType.DRAW, 0).getSize()); assertEquals(Play, state.getGamePhase()); - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getBuysLeft()); assertTrue(state.isActionInProgress()); List availableActions = fm.computeAvailableActions(state); @@ -763,8 +763,8 @@ public void poacherWithTwoEmptyPiles() { int finalSpend = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); assertTrue(poacher.executionComplete(state)); assertEquals(Play, state.getGamePhase()); - assertEquals(finalSpend + 1, state.availableSpend(0)); - assertEquals(1, state.actionsLeft()); + assertEquals(finalSpend + 1, state.getAvailableSpend(0)); + assertEquals(1, state.getActionsLeft()); } @@ -875,7 +875,7 @@ public void harbingerWithNoDiscard() { fm.next(state, harbinger); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); assertNull(state.currentActionInProgress()); assertEquals(Play, state.getGamePhase()); @@ -934,15 +934,15 @@ public void throneRoomWithMarket() { ThroneRoom throneRoom = new ThroneRoom(0); fm.next(state, throneRoom); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); assertEquals(throneRoom, state.currentActionInProgress()); List nextActions = fm.computeAvailableActions(state); assertEquals(1, nextActions.size()); assertEquals(DominionCard.create(CardType.MARKET).getAction(0), nextActions.get(0)); fm.next(state, nextActions.get(0)); - assertEquals(1, state.actionsLeft()); - assertEquals(2, state.buysLeft()); + assertEquals(1, state.getActionsLeft()); + assertEquals(2, state.getBuysLeft()); assertEquals(2, state.getDeck(DeckType.TABLE, 0).getSize()); assertEquals(6, state.getDeck(DeckType.HAND, 0).getSize()); @@ -952,8 +952,8 @@ public void throneRoomWithMarket() { assertNotEquals(DominionCard.create(CardType.MARKET).getAction(0, false), nextActions.get(0)); fm.next(state, nextActions.get(0)); - assertEquals(2, state.actionsLeft()); // we used our action on th eThrone Room, and then each Market gives +1 Action - assertEquals(3, state.buysLeft()); + assertEquals(2, state.getActionsLeft()); // we used our action on th eThrone Room, and then each Market gives +1 Action + assertEquals(3, state.getBuysLeft()); assertEquals(2, state.getDeck(DeckType.TABLE, 0).getSize()); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); assertFalse(state.isActionInProgress()); @@ -966,7 +966,7 @@ public void throneRoomWithNoActions() { state.addCard(CardType.THRONE_ROOM, 0, DeckType.HAND); ThroneRoom throneRoom = new ThroneRoom(0); fm.next(state, throneRoom); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); } @@ -985,7 +985,7 @@ public void throneRoomWithWorkshop() { fm.next(state, nextActions.get(0)); assertEquals(new Workshop(0), state.currentActionInProgress()); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); assertEquals(2, state.getDeck(DeckType.TABLE, 0).getSize()); assertEquals(5, state.getDeck(DeckType.HAND, 0).getSize()); @@ -1033,7 +1033,7 @@ public void throneRoomWithMerchant() { assertEquals(throneRoom, state.currentActionInProgress()); nextActions = fm.computeAvailableActions(state); assertEquals(new Merchant(0, true), nextActions.get(0)); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); fm.next(state, nextActions.get(0)); @@ -1041,13 +1041,13 @@ public void throneRoomWithMerchant() { assertEquals(Play, state.getGamePhase()); nextActions = fm.computeAvailableActions(state); assertEquals(1, nextActions.size()); - assertEquals(2, state.actionsLeft()); + assertEquals(2, state.getActionsLeft()); assertEquals(8, state.getDeck(DeckType.HAND, 0).getSize()); fm.next(state, new EndPhase(Play)); assertEquals(DominionGamePhase.Buy, state.getGamePhase()); int treasureInHand = state.getDeck(DeckType.HAND, 0).sumInt(DominionCard::treasureValue); - assertEquals(treasureInHand + 2, state.availableSpend(0)); + assertEquals(treasureInHand + 2, state.getAvailableSpend(0)); } @Test @@ -1066,7 +1066,7 @@ public void throneRoomWithThroneRoomWithSingleMarket() { assertEquals(DominionCard.create(CardType.MARKET).getAction(0), nextActions.get(1)); fm.next(state, nextActions.get(0)); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); assertNotSame(throneRoom, state.currentActionInProgress()); assertTrue(state.currentActionInProgress() instanceof ThroneRoom); // we now have the second throne room controlling the action flow @@ -1081,7 +1081,7 @@ public void throneRoomWithThroneRoomWithSingleMarket() { assertEquals(1, nextActions.size()); assertEquals(new EndPhase(Play), nextActions.get(0)); // EnthroneMarket - I - assertEquals(3, state.buysLeft()); + assertEquals(3, state.getBuysLeft()); assertEquals(3, state.getDeck(DeckType.TABLE, 0).getSize()); assertEquals(7, state.getDeck(DeckType.HAND, 0).getSize()); } @@ -1306,7 +1306,7 @@ public void sentryWithNoneKept() { assertEquals(sentry, state.currentActionInProgress()); assertEquals(0, state.getCurrentPlayer()); - assertEquals(1, state.actionsLeft()); + assertEquals(1, state.getActionsLeft()); assertEquals(8, state.getDeck(DeckType.HAND, 0).getSize()); assertEquals(2, state.getDeck(DeckType.DRAW, 0).getSize()); List nextActions = fm.computeAvailableActions(state); diff --git a/src/test/java/games/dominion/TestCoreGameLoop.java b/src/test/java/games/dominion/TestCoreGameLoop.java index cda36ed38..3d5b7c958 100644 --- a/src/test/java/games/dominion/TestCoreGameLoop.java +++ b/src/test/java/games/dominion/TestCoreGameLoop.java @@ -53,9 +53,9 @@ public void purchaseOptionsAreCorrect() { DominionGameState state = (DominionGameState) game.getGameState(); state.setGamePhase(DominionGamePhase.Buy); for (int i = 0; i < 10; i++) { - state.spend(state.availableSpend(0)); + state.spend(state.getAvailableSpend(0)); state.spend(-i); - assertEquals(i, state.availableSpend(0)); + assertEquals(i, state.getAvailableSpend(0)); List actions = fm.computeAvailableActions(state); Set availableCards = actions.stream() .filter(a -> a instanceof BuyCard) @@ -157,7 +157,7 @@ public void buyingACard() { assertEquals(0, state.cardsOfType(CardType.SILVER, 0, DeckType.HAND)); assertEquals(0, state.cardsOfType(CardType.SILVER, 0, DeckType.DRAW)); assertEquals(silverAvailable - 1, state.cardsOfType(CardType.SILVER, 0, DeckType.SUPPLY)); - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getBuysLeft()); assertEquals(DominionGamePhase.Play, state.getGamePhase()); } @@ -168,7 +168,7 @@ public void canBuyMoreThanOneCard() { BuyCard newBuy = new BuyCard(CardType.COPPER, 0); state.changeBuys(3); for (int i = 0; i < 4; i++) { - assertEquals(4 - i, state.buysLeft()); + assertEquals(4 - i, state.getBuysLeft()); assertEquals(i, state.cardsOfType(CardType.COPPER, 0, DeckType.DISCARD)); List actions = fm.computeAvailableActions(state); assertTrue(actions.contains(new EndPhase(DominionGamePhase.Buy))); @@ -176,7 +176,7 @@ public void canBuyMoreThanOneCard() { fm.computeAvailableActions(state); fm.next(state, newBuy); } - assertEquals(1, state.buysLeft()); + assertEquals(1, state.getBuysLeft()); assertEquals(DominionGamePhase.Play, state.getGamePhase()); assertEquals(1, state.getCurrentPlayer()); assertEquals(11, state.cardsOfType(CardType.COPPER, 0, DeckType.ALL)); @@ -240,7 +240,7 @@ public void cannotPlayACardWithNoActionsLeft() { state.addCard(CardType.VILLAGE, 0, DeckType.HAND); state.addCard(CardType.SMITHY, 0, DeckType.HAND); (new SimpleAction(CardType.SMITHY, 0)).execute(state); - assertEquals(0, state.actionsLeft()); + assertEquals(0, state.getActionsLeft()); List actions = fm.computeAvailableActions(state); assertEquals(1, actions.size()); assertTrue(actions.contains(new EndPhase(DominionGamePhase.Play))); diff --git a/src/test/java/games/dominion/TestFullObservabilityCopy.java b/src/test/java/games/dominion/TestFullObservabilityCopy.java index e0d58990a..fef0c6377 100644 --- a/src/test/java/games/dominion/TestFullObservabilityCopy.java +++ b/src/test/java/games/dominion/TestFullObservabilityCopy.java @@ -4,9 +4,6 @@ import core.components.PartialObservableDeck; import games.GameType; import games.dominion.DominionConstants.DeckType; -import games.dominion.DominionForwardModel; -import games.dominion.DominionGameState; -import games.dominion.DominionParameters; import games.dominion.actions.AttackReaction; import games.dominion.actions.DominionAction; import games.dominion.actions.EndPhase; diff --git a/src/test/java/games/explodingkittens/BasicGameTurn.java b/src/test/java/games/explodingkittens/BasicGameTurn.java new file mode 100644 index 000000000..4a36dea97 --- /dev/null +++ b/src/test/java/games/explodingkittens/BasicGameTurn.java @@ -0,0 +1,173 @@ +package games.explodingkittens; + +import core.CoreConstants; +import core.actions.AbstractAction; +import games.explodingkittens.actions.DefuseKitten; +import games.explodingkittens.actions.Pass; +import games.explodingkittens.actions.PlaceKitten; +import games.explodingkittens.actions.PlayEKCard; +import games.explodingkittens.cards.ExplodingKittensCard; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Random; + +import static games.explodingkittens.cards.ExplodingKittensCard.CardType.*; +import static org.junit.Assert.*; + +public class BasicGameTurn { + + ExplodingKittensGameState state; + ExplodingKittensForwardModel fm; + ExplodingKittensParameters params; + Random rnd = new Random(4033); + + @Before + public void init() { + params = new ExplodingKittensParameters(); + // this removes any cards which have extra action decisions to make + params.cardCounts.put(ATTACK, 0); + params.cardCounts.put(SKIP, 0); + params.cardCounts.put(FAVOR, 0); + params.cardCounts.put(NOPE, 0); + state = new ExplodingKittensGameState(params, 4); + fm = new ExplodingKittensForwardModel(); + fm.setup(state); + } + + + @Test + public void setupTest() { + // check that every player has at least on DEFUSE card (and 8 cards in total) + // and that the draw pile contains 3 Exploding kittens + // and that the discard pile is empty + + for (int player = 0; player < state.getNPlayers(); player++) { + assertEquals(8, state.playerHandCards.get(player).getSize()); + assertTrue(state.playerHandCards.get(player).stream().filter(c -> c.cardType == ExplodingKittensCard.CardType.DEFUSE).count() == 1); + } + assertEquals(0, state.discardPile.getSize()); + assertEquals(3, state.drawPile.stream().filter(c -> c.cardType == ExplodingKittensCard.CardType.EXPLODING_KITTEN).count()); + } + + @Test + public void basicTurnSequenceTest() { + // Each player should play or pass until the game ends + int expectedPlayer = 0; + do { + System.out.println("Player " + expectedPlayer + " playing"); + assertEquals(expectedPlayer, state.getCurrentPlayer()); + List actions = fm.computeAvailableActions(state); + fm.next(state, actions.get(rnd.nextInt(actions.size()))); + if (!state.isActionInProgress()) + do { + expectedPlayer = (expectedPlayer + 1) % state.getNPlayers(); + } while (state.getPlayerResults()[expectedPlayer] == CoreConstants.GameResult.LOSE_GAME); + } while (state.isNotTerminal()); + } + + @Test + public void passActionDrawsCard() { + state.drawPile.add(new ExplodingKittensCard(TACOCAT)); + fm.next(state, new Pass()); + assertEquals(9, state.playerHandCards.get(0).getSize()); + assertEquals(0, state.discardPile.getSize()); + } + + @Test + public void playActionDrawsCard() { + state.drawPile.add(new ExplodingKittensCard(TACOCAT)); + state.drawPile.add(new ExplodingKittensCard(TACOCAT)); + state.playerHandCards.get(0).add(new ExplodingKittensCard(SHUFFLE)); + assertEquals(9, state.playerHandCards.get(0).getSize()); + + List actions = fm.computeAvailableActions(state); + assertEquals(new Pass(), actions.get(0)); + fm.next(state, new PlayEKCard(SHUFFLE)); + if (state.getDiscardPile().getSize() == 2) { + assertTrue(state.discardPile.stream().anyMatch(c -> c.cardType == DEFUSE)); + assertTrue(state.discardPile.stream().anyMatch(c -> c.cardType == SHUFFLE)); + assertEquals(8, state.playerHandCards.get(0).getSize()); + } else { + assertEquals(1, state.discardPile.getSize()); + assertEquals(9, state.playerHandCards.get(0).getSize()); + } +; + } + + @Test + public void defuseStopsExplodingKitten() { + state.drawPile.add(new ExplodingKittensCard(EXPLODING_KITTEN)); + assertTrue(state.getPlayerHand(0).stream().anyMatch(c -> c.cardType == DEFUSE)); + fm.next(state, new Pass()); + assertTrue(state.isNotTerminalForPlayer(0)); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.discardPile.getSize()); + assertEquals(3, state.drawPile.stream().filter(c -> c.cardType == EXPLODING_KITTEN).count()); + } + + @Test + public void defuseGivesChoiceOfPlacementToPlayer() { + state.drawPile.add(new ExplodingKittensCard(EXPLODING_KITTEN)); + assertTrue(state.getPlayerHand(0).stream().anyMatch(c -> c.cardType == DEFUSE)); + fm.next(state, new Pass()); + assertTrue(state.isActionInProgress()); + assertEquals(0, state.getCurrentPlayer()); + assertTrue(state.currentActionInProgress() instanceof DefuseKitten); + + List actions = fm.computeAvailableActions(state); + assertEquals(state.drawPile.getSize()+1, actions.size()); + assertTrue(actions.stream().allMatch(a -> a instanceof PlaceKitten)); + + assertEquals(1, state.discardPile.getSize()); + int index = ((PlaceKitten) actions.get(3)).index; + fm.next(state, actions.get(3)); + + assertEquals(1, state.discardPile.getSize()); + assertEquals(4, state.drawPile.stream().filter(c -> c.cardType == EXPLODING_KITTEN).count()); + assertEquals(EXPLODING_KITTEN, state.drawPile.get(index).cardType); + assertTrue(state.drawPile.getVisibilityForPlayer(index, 0)); + assertFalse(state.drawPile.getVisibilityForPlayer(index, 1)); + assertFalse(state.drawPile.getVisibilityForPlayer(index, 2)); + assertFalse(state.drawPile.getVisibilityForPlayer(index, 3)); + + assertEquals(0, state.getActionsInProgress().size()); + assertEquals(1, state.getCurrentPlayer()); + } + + @Test + public void defusingDoesNotDrawExtraCard() { + state.drawPile.add(new ExplodingKittensCard(EXPLODING_KITTEN)); + fm.next(state, new Pass()); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.discardPile.getSize()); + assertEquals(8, state.playerHandCards.get(0).getSize()); // +KITTEN -DEFUSE + fm.next(state, new PlaceKitten(3)); + assertEquals(1, state.discardPile.getSize()); + assertEquals(4, state.drawPile.stream().filter(c -> c.cardType == EXPLODING_KITTEN).count()); + } + + @Test + public void explodingKittenKillsPlayer() { + state.drawPile.add(new ExplodingKittensCard(EXPLODING_KITTEN)); + List defuseCards = state.playerHandCards.get(0).stream().filter(c -> c.cardType == DEFUSE).toList(); + state.playerHandCards.get(0).removeAll(defuseCards); + assertTrue(state.getPlayerHand(0).stream().noneMatch(c -> c.cardType == DEFUSE)); + fm.next(state, new Pass()); + assertFalse(state.isNotTerminalForPlayer(0)); + assertEquals(1, state.discardPile.getSize()); + assertEquals(3, state.drawPile.stream().filter(c -> c.cardType == EXPLODING_KITTEN).count()); + } + + @Test + public void deadPlayerMissesAllFutureTurns() { + fm.killPlayer(state, 2); + do { + assertNotEquals(2, state.getCurrentPlayer()); + List actions = fm.computeAvailableActions(state); + fm.next(state, actions.get(rnd.nextInt(actions.size()))); + } while (state.isNotTerminal()); + } + +} diff --git a/src/test/java/games/explodingkittens/NopeInteractions.java b/src/test/java/games/explodingkittens/NopeInteractions.java new file mode 100644 index 000000000..189f23f5b --- /dev/null +++ b/src/test/java/games/explodingkittens/NopeInteractions.java @@ -0,0 +1,157 @@ +package games.explodingkittens; + +import core.actions.AbstractAction; +import games.explodingkittens.actions.ChoiceOfCardToGive; +import games.explodingkittens.actions.Nope; +import games.explodingkittens.actions.Pass; +import games.explodingkittens.actions.PlayEKCard; +import games.explodingkittens.cards.ExplodingKittensCard; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import static games.explodingkittens.cards.ExplodingKittensCard.CardType.*; +import static org.junit.Assert.*; + +public class NopeInteractions { + + + ExplodingKittensGameState state; + ExplodingKittensForwardModel fm; + ExplodingKittensParameters params; + + @Before + public void init() { + params = new ExplodingKittensParameters(); + // this removes any cards which have extra action decisions to make + params.cardCounts.put(ATTACK, 0); + params.cardCounts.put(SKIP, 0); + params.cardCounts.put(FAVOR, 0); + params.cardCounts.put(NOPE, 0); // we want to add these manually + state = new ExplodingKittensGameState(params, 4); + fm = new ExplodingKittensForwardModel(); + fm.setup(state); + state.drawPile.add(new ExplodingKittensCard(TACOCAT)); + state.drawPile.add(new ExplodingKittensCard(RAINBOWCAT)); + state.drawPile.add(new ExplodingKittensCard(BEARDCAT)); + } + + @Test + public void nopeNotPlayableByItself() { + state.getPlayerHand(0).clear(); + state.getPlayerHand(0).add(new ExplodingKittensCard(NOPE)); + List actions = fm.computeAvailableActions(state); + assertEquals(1, actions.size()); + assertEquals(new Pass(), actions.get(0)); + } + + + @Test + public void interruptOptionForNopePlayers() { + state.getPlayerHand(2).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + assertEquals(8, state.getPlayerHand(1).getSize()); + + fm.next(state, new PlayEKCard(FAVOR, 1)); + assertTrue(state.isActionInProgress()); + assertEquals(2, state.getCurrentPlayer()); + + List actions = fm.computeAvailableActions(state); + assertEquals(2, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new Nope(), actions.get(1)); + + fm.next(state, new Pass()); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.getCurrentPlayer()); + assertTrue(state.currentActionInProgress() instanceof ChoiceOfCardToGive); + } + + @Test + public void deadPlayerDoesNotGetANopeOption() { + state.getPlayerHand(2).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + + fm.killPlayer(state, 2); + + fm.next(state, new PlayEKCard(FAVOR, 1)); + assertTrue(state.isActionInProgress()); + assertTrue(state.currentActionInProgress() instanceof ChoiceOfCardToGive); + assertEquals(1, state.getCurrentPlayer()); + } + + @Test + public void nopingACardMeansItHasNoEffect() { + state.getPlayerHand(2).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + assertEquals(8, state.getPlayerHand(1).getSize()); + + fm.next(state, new PlayEKCard(FAVOR, 1)); + assertTrue(state.isActionInProgress()); + assertEquals(2, state.getCurrentPlayer()); + + fm.next(state, new Nope()); + // and also that Nope card is discarded + assertFalse(state.isActionInProgress()); // because no other NOPE cards + assertEquals(2, state.discardPile.getSize()); + assertEquals(1, state.getCurrentPlayer()); + assertEquals(8, state.getPlayerHand(1).getSize()); + } + + @Test + public void nopingANopeCardMeansCardHasEffect() { + state.getPlayerHand(2).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + state.getPlayerHand(0).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(1).add(new ExplodingKittensCard(NOPE)); + assertEquals(9, state.getPlayerHand(1).getSize()); + + fm.next(state, new PlayEKCard(FAVOR, 1)); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.getCurrentPlayer()); + fm.next(state, new Pass()); // player 1 passes + assertEquals(2, state.getCurrentPlayer()); + + fm.next(state, new Nope()); + assertTrue(state.isActionInProgress()); + assertEquals(0, state.getCurrentPlayer()); + fm.next(state, new Nope()); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.getCurrentPlayer()); + fm.next(state, new Pass()); // player 1 passes again + + assertTrue(state.isActionInProgress()); + assertEquals(1, state.getCurrentPlayer()); + assertTrue(state.currentActionInProgress() instanceof ChoiceOfCardToGive); + } + + @Test + public void nopingThreeFoldRecursionHasNoEffect() { + state.getPlayerHand(2).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + state.getPlayerHand(0).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(1).add(new ExplodingKittensCard(NOPE)); + fm.next(state, new PlayEKCard(FAVOR, 1)); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.getCurrentPlayer()); + fm.next(state, new Pass()); // player 1 passes + assertEquals(2, state.getCurrentPlayer()); + + fm.next(state, new Nope()); + // and also that Nope card is discarded + assertTrue(state.isActionInProgress()); + assertEquals(0, state.getCurrentPlayer()); + fm.next(state, new Nope()); + assertTrue(state.isActionInProgress()); + assertEquals(1, state.getCurrentPlayer()); + fm.next(state, new Nope()); // player 1 now Nopes + + assertFalse(state.isActionInProgress()); // because no other NOPE cards + + assertEquals(4, state.discardPile.getSize()); // FAVOR and 3 NOPE + assertEquals(1, state.getCurrentPlayer()); + assertEquals(8, state.getPlayerHand(1).getSize()); + } + +} diff --git a/src/test/java/games/explodingkittens/SpecialCards.java b/src/test/java/games/explodingkittens/SpecialCards.java new file mode 100644 index 000000000..89c7f6b8f --- /dev/null +++ b/src/test/java/games/explodingkittens/SpecialCards.java @@ -0,0 +1,295 @@ +package games.explodingkittens; + +import core.actions.AbstractAction; +import core.components.Deck; +import games.explodingkittens.actions.*; +import games.explodingkittens.cards.ExplodingKittensCard; +import games.hearts.actions.Play; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import static core.CoreConstants.GameResult.LOSE_GAME; +import static games.explodingkittens.cards.ExplodingKittensCard.CardType.*; +import static org.junit.Assert.*; + +public class SpecialCards { + + ExplodingKittensGameState state; + ExplodingKittensForwardModel fm; + ExplodingKittensParameters params; + + @Before + public void init() { + params = new ExplodingKittensParameters(); + // this removes any cards which have extra action decisions to make + params.cardCounts.put(ATTACK, 0); + params.cardCounts.put(SKIP, 0); + params.cardCounts.put(FAVOR, 0); + params.cardCounts.put(NOPE, 0); // we want to add these manually + state = new ExplodingKittensGameState(params, 4); + fm = new ExplodingKittensForwardModel(); + fm.setup(state); + state.drawPile.add(new ExplodingKittensCard(TACOCAT)); + state.drawPile.add(new ExplodingKittensCard(RAINBOWCAT)); + state.drawPile.add(new ExplodingKittensCard(BEARDCAT)); + } + + @Test + public void favourActions() { + state.getPlayerHand(0).clear(); + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + List actions = fm.computeAvailableActions(state); + assertEquals(4, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new PlayEKCard(FAVOR, 1), actions.get(1)); + assertEquals(new PlayEKCard(FAVOR, 2), actions.get(2)); + assertEquals(new PlayEKCard(FAVOR, 3), actions.get(3)); + } + + @Test + public void favourFunctionality() { + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + assertEquals(9, state.getPlayerHand(0).getSize()); + assertEquals(8, state.getPlayerHand(1).getSize()); + fm.next(state, new PlayEKCard(FAVOR, 1)); + assertEquals(1, state.getCurrentPlayer()); + assertTrue(state.isActionInProgress()); + List actions = fm.computeAvailableActions(state); + assertEquals(state.getPlayerHand(1).stream().map(c -> c.cardType).distinct().count(), actions.size()); + fm.next(state, new GiveCard(1, 0, state.getPlayerHand(1).get(0).cardType)); + assertFalse(state.isActionInProgress()); + + assertEquals(10, state.getPlayerHand(0).getSize()); + assertEquals(7, state.getPlayerHand(1).getSize()); + } + + @Test + public void cannotAskFavourOfTheDead() { + state.getPlayerHand(0).add(new ExplodingKittensCard(FAVOR)); + List actions = fm.computeAvailableActions(state); + assertTrue(actions.contains(new PlayEKCard(FAVOR, 1))); + fm.killPlayer(state, 1); + actions = fm.computeAvailableActions(state); + assertFalse(actions.contains(new PlayEKCard(FAVOR, 1))); + } + + @Test + public void shuffle() { + state.getPlayerHand(0).clear(); + ExplodingKittensCard shuffleCard = new ExplodingKittensCard(SHUFFLE); + state.getPlayerHand(0).add(shuffleCard); + List actions = fm.computeAvailableActions(state); + assertEquals(2, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new PlayEKCard(SHUFFLE), actions.get(1)); + + Deck oldDrawPile = state.drawPile.copy(); + fm.next(state, new PlayEKCard(SHUFFLE)); + assertEquals(oldDrawPile.getSize() - 1, state.drawPile.getSize()); + boolean allSame = true; + for (int i = 1; i < oldDrawPile.getSize(); i++) { + if (oldDrawPile.get(i) != state.drawPile.get(i)) { + allSame = false; + break; + } + } + assertFalse(allSame); + boolean containsAll = true; + for (ExplodingKittensCard c : state.drawPile) { + if (!oldDrawPile.contains(c)) { + containsAll = false; + break; + } + oldDrawPile.remove(c); + } + assertTrue(containsAll); + } + + @Test + public void skip() { + state.getPlayerHand(0).clear(); + ExplodingKittensCard shuffleCard = new ExplodingKittensCard(SKIP); + state.getPlayerHand(0).add(shuffleCard); + List actions = fm.computeAvailableActions(state); + assertEquals(2, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new PlayEKCard(SKIP), actions.get(1)); + int drawDeck = state.drawPile.getSize(); + + assertFalse(state.skip); + fm.next(state, new PlayEKCard(SKIP)); + assertFalse(state.skip); + + assertEquals(1, state.getCurrentPlayer()); + assertEquals(0, state.getPlayerHand(0).getSize()); + assertEquals(drawDeck, state.drawPile.getSize()); + } + + @Test + public void attack() { + state.getPlayerHand(0).clear(); + ExplodingKittensCard shuffleCard = new ExplodingKittensCard(ATTACK); + state.getPlayerHand(0).add(shuffleCard); + List actions = fm.computeAvailableActions(state); + assertEquals(2, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new PlayEKCard(ATTACK), actions.get(1)); + int drawDeck = state.drawPile.getSize(); + + fm.next(state, new PlayEKCard(ATTACK)); + assertEquals(drawDeck, state.drawPile.getSize()); + assertEquals(2, state.currentPlayerTurnsLeft); + assertEquals(1, state.getCurrentPlayer()); + + fm.next(state, new Pass()); + assertEquals(1, state.currentPlayerTurnsLeft); + assertEquals(1, state.getCurrentPlayer()); + fm.next(state, new Pass()); + + assertEquals(1, state.currentPlayerTurnsLeft); + assertEquals(2, state.getCurrentPlayer()); + assertEquals(drawDeck - 2, state.drawPile.getSize()); + } + + @Test + public void deathInMiddleOfDoubleMove() { + ExplodingKittensCard attackCard = new ExplodingKittensCard(ATTACK); + state.getPlayerHand(0).add(attackCard); + state.getPlayerHand(1).clear(); // remove DEFUSE card + + fm.next(state, new PlayEKCard(ATTACK)); + assertEquals(2, state.currentPlayerTurnsLeft); + assertEquals(1, state.getCurrentPlayer()); + + state.drawPile.add(new ExplodingKittensCard(EXPLODING_KITTEN)); + fm.next(state, new Pass()); + + assertEquals(LOSE_GAME, state.getPlayerResults()[1]); + assertEquals(1, state.currentPlayerTurnsLeft); + assertEquals(2, state.getCurrentPlayer()); + } + + @Test + public void doubleAttack() { + ExplodingKittensCard attackCard = new ExplodingKittensCard(ATTACK); + state.getPlayerHand(0).add(attackCard); + + fm.next(state, new PlayEKCard(ATTACK)); + assertEquals(2, state.currentPlayerTurnsLeft); + assertEquals(1, state.getCurrentPlayer()); + + state.getPlayerHand(1).add(attackCard); + fm.next(state, new PlayEKCard(ATTACK)); + + assertEquals(4, state.currentPlayerTurnsLeft); + assertEquals(2, state.getCurrentPlayer()); + } + + @Test + public void attackAndNope() { + ExplodingKittensCard attackCard = new ExplodingKittensCard(ATTACK); + state.getPlayerHand(0).add(attackCard); + ExplodingKittensCard nopeCard = new ExplodingKittensCard(NOPE); + state.getPlayerHand(1).add(nopeCard); + int drawDeck = state.drawPile.getSize(); + + fm.next(state, new PlayEKCard(ATTACK)); + assertEquals(1, state.getCurrentPlayer()); + assertEquals(0, state.getTurnOwner()); + + fm.next(state, new Nope()); + assertEquals(1, state.getCurrentPlayer()); + assertEquals(1, state.getTurnOwner()); + assertEquals(drawDeck - 1, state.drawPile.getSize()); + + assertEquals(1, state.currentPlayerTurnsLeft); + } + + @Test + public void attackAndPotentialNopeLoop() { + ExplodingKittensCard attackCard = new ExplodingKittensCard(ATTACK); + state.getPlayerHand(0).add(attackCard); + state.getPlayerHand(1).add(new ExplodingKittensCard(NOPE)); + state.getPlayerHand(2).add(new ExplodingKittensCard(NOPE)); + + fm.next(state, new PlayEKCard(ATTACK)); + fm.next(state, new Pass()); + fm.next(state, new Pass()); + assertEquals(1, state.getCurrentPlayer()); + assertFalse(state.isActionInProgress()); + assertEquals(2, state.currentPlayerTurnsLeft); + } + + @Test + public void seeTheFuture() { + state.getPlayerHand(0).clear(); + ExplodingKittensCard seeTheFutureCard = new ExplodingKittensCard(SEETHEFUTURE); + state.getPlayerHand(0).add(seeTheFutureCard); + int drawDeck = state.drawPile.getSize(); + + List actions = fm.computeAvailableActions(state); + assertEquals(2, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new PlayEKCard(SEETHEFUTURE), actions.get(1)); + + fm.next(state, new PlayEKCard(SEETHEFUTURE)); + assertEquals(1, state.getCurrentPlayer()); + assertEquals(1, state.getTurnOwner()); + assertEquals(drawDeck - 1, state.drawPile.getSize()); + + // and check drawpile visibility + for (int i = 0; i < state.drawPile.getSize(); i++) { + if (i < 2) // only the top 2 cards should be visible, as we drew the top one + assertTrue(state.drawPile.getVisibilityForPlayer(i, 0)); + else + assertFalse(state.drawPile.getVisibilityForPlayer(i, 0)); + + assertFalse(state.drawPile.getVisibilityForPlayer(i, 1)); + assertFalse(state.drawPile.getVisibilityForPlayer(i, 2)); + assertFalse(state.drawPile.getVisibilityForPlayer(i, 3)); + } + } + + @Test + public void needTwoCatCardsToPlay() { + state.getPlayerHand(0).clear(); + state.getPlayerHand(0).add(new ExplodingKittensCard(TACOCAT)); + state.getPlayerHand(0).add(new ExplodingKittensCard(RAINBOWCAT)); + List actions = fm.computeAvailableActions(state); + assertEquals(1, actions.size()); + assertEquals(new Pass(), actions.get(0)); + + state.getPlayerHand(0).add(new ExplodingKittensCard(TACOCAT)); + actions = fm.computeAvailableActions(state); + assertEquals(4, actions.size()); + assertEquals(new Pass(), actions.get(0)); + assertEquals(new PlayEKCard(TACOCAT, 1), actions.get(1)); + assertEquals(new PlayEKCard(TACOCAT, 2), actions.get(2)); + + assertEquals(3, state.getPlayerHand(0).getSize()); + fm.next(state, new PlayEKCard(TACOCAT, 1)); + assertEquals(1, state.getCurrentPlayer()); + assertFalse(state.isActionInProgress()); + assertEquals(2, state.getDiscardPile().getSize()); + assertTrue(state.getDiscardPile().stream().anyMatch(c -> c.cardType == TACOCAT)); + assertEquals(3, state.getPlayerHand(0).getSize()); + assertEquals(7, state.getPlayerHand(1).getSize()); + } + + @Test + public void nopeTwoCatCardsDiscardsBoth() { + state.getPlayerHand(0).add(new ExplodingKittensCard(TACOCAT)); + state.getPlayerHand(0).add(new ExplodingKittensCard(TACOCAT)); + state.getPlayerHand(3).add(new ExplodingKittensCard(NOPE)); + + fm.next(state, new PlayEKCard(TACOCAT, 3)); + assertTrue(state.isActionInProgress()); + fm.next(state, new Nope()); + assertFalse(state.isActionInProgress()); + + assertEquals(3, state.getDiscardPile().getSize()); // 2 x TACOCAT + NOPE + assertEquals(9, state.getPlayerHand(0).getSize()); + } +} diff --git a/src/test/java/games/fmtester/ForwardModelTestsWithMCTS.java b/src/test/java/games/fmtester/ForwardModelTestsWithMCTS.java index b74bb3196..dba4fb428 100644 --- a/src/test/java/games/fmtester/ForwardModelTestsWithMCTS.java +++ b/src/test/java/games/fmtester/ForwardModelTestsWithMCTS.java @@ -49,7 +49,7 @@ public void testDotsAndBoxes() { } @Test public void testExplodingKittens() { - new ForwardModelTester("game=ExplodingKittens", "nGames=1", "nPlayers=3", "agent=json\\players\\gameSpecific\\ExplodingKittens.json"); + new ForwardModelTester("game=ExplodingKittens", "nGames=3", "nPlayers=3", "agent=json\\players\\gameSpecific\\ExplodingKittens.json"); } @Test public void testLoveLetter() { diff --git a/src/test/java/games/fmtester/ForwardModelTestsWithRandom.java b/src/test/java/games/fmtester/ForwardModelTestsWithRandom.java index 71b6a5fd3..cb9a72bcb 100644 --- a/src/test/java/games/fmtester/ForwardModelTestsWithRandom.java +++ b/src/test/java/games/fmtester/ForwardModelTestsWithRandom.java @@ -33,10 +33,6 @@ public void testDiamant() { public void testDominion() { ForwardModelTester fmt = new ForwardModelTester("game=Dominion", "nGames=2", "nPlayers=3"); } -// @Test -// public void testDiceMonastery() { -// ForwardModelTester fmt = new ForwardModelTester("game=DiceMonastery", "nGames=2", "nPlayers=3"); -// } @Test public void testDotsAndBoxes() { ForwardModelTester fmt = new ForwardModelTester("game=DotsAndBoxes", "nGames=2", "nPlayers=3"); @@ -49,10 +45,6 @@ public void testExplodingKittens() { public void testLoveLetter() { ForwardModelTester fmt = new ForwardModelTester("game=LoveLetter", "nGames=2", "nPlayers=3"); } -// @Test -// public void testPandemic() { -// ForwardModelTester fmt = new ForwardModelTester("game=Pandemic", "nGames=2", "nPlayers=3"); -// } @Test public void testPoker() { ForwardModelTester fmt = new ForwardModelTester("game=Poker", "nGames=2", "nPlayers=3"); diff --git a/src/test/java/players/heuristics/TestStateHeuristics.java b/src/test/java/players/heuristics/TestStateHeuristics.java index 94b28d0aa..0dc71e018 100644 --- a/src/test/java/players/heuristics/TestStateHeuristics.java +++ b/src/test/java/players/heuristics/TestStateHeuristics.java @@ -16,7 +16,6 @@ import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; import java.util.List; import static org.junit.Assert.assertEquals; @@ -96,8 +95,8 @@ public void testLogisticStateHeuristic() { @Test public void testActionHeuristic() { llState.getPlayerHandCards().get(0).clear(); - llState.getPlayerHandCards().get(0).add(new LoveLetterCard(LoveLetterCard.CardType.Handmaid)); - llState.getPlayerHandCards().get(0).add(new LoveLetterCard(LoveLetterCard.CardType.Guard)); + llState.getPlayerHandCards().get(0).add(new LoveLetterCard(games.loveletter.cards.CardType.Handmaid)); + llState.getPlayerHandCards().get(0).add(new LoveLetterCard(games.loveletter.cards.CardType.Guard)); LinearActionHeuristic linearActionHeuristic = new LinearActionHeuristic(llActionFeaturesTiny, llStateFeaturesReduced, "src\\test\\java\\players\\heuristics\\LLFeatureWeights.json") { }; @@ -109,27 +108,27 @@ public void testActionHeuristic() { // Hand should contain a Handmaid and a Guard (Total Value = 5) for (AbstractAction action : actions) { PlayCard playCard = (PlayCard) action; - LoveLetterCard.CardType cardType = playCard.getCardType(); - if (cardType == LoveLetterCard.CardType.Guard) + games.loveletter.cards.CardType cardType = playCard.getCardType(); + if (cardType == games.loveletter.cards.CardType.Guard) assertEquals(0.1 * 5.0 / 17.0 + 0.2 + 0.01 + 10.0, linearActionHeuristic.evaluateAction(action, llState), 0.0001); - else if (cardType == LoveLetterCard.CardType.Handmaid) + else if (cardType == games.loveletter.cards.CardType.Handmaid) assertEquals(0.1 * 5.0 / 17.0 + 0.2 + 10.0, linearActionHeuristic.evaluateAction(action, llState), 0.0001); else throw new AssertionError("Unexpected action: " + action); } llState.getPlayerHandCards().get(0).clear(); - llState.getPlayerHandCards().get(0).add(new LoveLetterCard(LoveLetterCard.CardType.Baron)); - llState.getPlayerHandCards().get(0).add(new LoveLetterCard(LoveLetterCard.CardType.Guard)); + llState.getPlayerHandCards().get(0).add(new LoveLetterCard(games.loveletter.cards.CardType.Baron)); + llState.getPlayerHandCards().get(0).add(new LoveLetterCard(games.loveletter.cards.CardType.Guard)); actions = llFm.computeAvailableActions(llState); // Hand should contain a Baron and a Guard (Total Value = 4) for (AbstractAction action : actions) { PlayCard playCard = (PlayCard) action; - LoveLetterCard.CardType cardType = playCard.getCardType(); - if (cardType == LoveLetterCard.CardType.Guard) + games.loveletter.cards.CardType cardType = playCard.getCardType(); + if (cardType == games.loveletter.cards.CardType.Guard) assertEquals(0.1 * 4.0 / 17.0 + 0.2 + 0.01 + 10.0 + 0.07, linearActionHeuristic.evaluateAction(action, llState), 0.0001); - else if (cardType == LoveLetterCard.CardType.Baron) + else if (cardType == games.loveletter.cards.CardType.Baron) assertEquals(0.1 * 4.0 / 17.0 + 0.2 + 10.0, linearActionHeuristic.evaluateAction(action, llState), 0.0001); else throw new AssertionError("Unexpected action: " + action);