Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>xyz.cliserkad</groupId>
<artifactId>smp</artifactId>
Expand Down
13 changes: 9 additions & 4 deletions src/main/java/xyz/cliserkad/consortium/Auction.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,29 @@
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class Auction implements Serializable {

@Serial
private static final long serialVersionUID = 20240714;
private static final long serialVersionUID = 20250309;

public final BoardElement property;
public final List<Player> bidders;
public Player currentBidder;
public List<BidAction> bids;
public int bid;

public Auction(BoardElement property, List<Player> bidders) {
// TODO: fix purchasable interface
public Auction(BoardElement property, List<Player> bidders, GameConfig config) {
this.property = property;
this.bidders = bidders;
bids = new ArrayList<>();
bid = 0;
if(property.position.logic instanceof Purchasable purchasable) {
bid = (int) (purchasable.cost() * config.startingBidFactor) - config.minimumBid;
} else {
System.err.println("Auction property is not purchasable.");
bid = 0;
}
}

}
35 changes: 23 additions & 12 deletions src/main/java/xyz/cliserkad/consortium/AutoGameClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@
*/
public class AutoGameClient implements GameClient {

// The bot will bid up to 80% of the property's market value
public static final double MAX_BID_RATIO = 0.75;
// bot will bid up to 125% of the market value
public static final double MAX_BID_RATIO = 1.25;

@Override
public PlayerAction poll(Player avatar, GameState gameState, Class<? extends PlayerAction> prompt) {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
// do nothing
}
if(prompt == PurchaseAction.class) {
// always try to purchase property
return new PurchaseAction(avatar.getPosition());
if(!canOthersBuy(gameState, avatar))
return null;
else
return new PurchaseAction(avatar.getPosition());
} else if(prompt == BidAction.class) {
// bid up the property if the other players are broke
if(!canOthersWinBid(gameState, avatar))
return new BidAction(gameState.getAuction().property.position, gameState.getAuction().bid + MIN_BID);
// bid up the property if the bid is less than bid ratio * market value
else if(bidLessThanMax(gameState))
if(bidLessThanMax(gameState))
return new BidAction(gameState.getAuction().property.position, gameState.getAuction().bid + MIN_BID);
else
return null;
Expand All @@ -29,7 +33,14 @@ else if(bidLessThanMax(gameState))
} else if(prompt == DeclareBankruptcyAction.class) {
return new DeclareBankruptcyAction();
} else if(prompt == AcceptTradeAction.class) {
return new AcceptTradeAction(gameState.getProposedTrade(), false);
// incoming value must be 2x the incoming value
int incomingValue = gameState.getProposedTrade().moneyToAcceptor;
for(BoardPosition position : gameState.getProposedTrade().positionsToAcceptor)
incomingValue += position.logic instanceof Purchasable purchasable ? purchasable.cost() : 0;
int outgoingValue = gameState.getProposedTrade().moneyToProposer;
for(BoardPosition position : gameState.getProposedTrade().positionsToProposer)
outgoingValue += position.logic instanceof Purchasable purchasable ? purchasable.cost() : 0;
return new AcceptTradeAction(gameState.getProposedTrade(), incomingValue >= 2 * outgoingValue);
} else {
return null;
}
Expand All @@ -45,11 +56,11 @@ public boolean bidLessThanMax(GameState gameState) {
/**
* Return false if any other player has more money than the position's market value
*/
public boolean canOthersWinBid(GameState gameState, Player avatar) {
public boolean canOthersBuy(GameState gameState, Player avatar) {
for(Player p : gameState.getPlayers()) {
if(p != avatar) {
if(gameState.getAuction().property.position.logic instanceof Purchasable purchasable) {
if(p.getMoney() > purchasable.cost()) {
if(gameState.getBoardElement(avatar).position.logic instanceof Purchasable purchasable) {
if(p.getMoney() > purchasable.cost() - 25) {
return true;
}
}
Expand Down
12 changes: 5 additions & 7 deletions src/main/java/xyz/cliserkad/consortium/BoardElementVisual.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;

import static xyz.cliserkad.consortium.Main.textColorForBackground;

Expand All @@ -17,8 +15,6 @@ public class BoardElementVisual extends JPanel implements GameStateReceiver {
private JLabel purchaseLabel;
private JLabel improvementLabel;

private List<PlayerVisual> playerVisuals = new ArrayList<>();

public BoardElementVisual(BoardElement element) {
super(new GridBagLayout());

Expand Down Expand Up @@ -75,11 +71,14 @@ public boolean update(GameState gameState) {
element = gameState.getBoardElement(element.position);

final int targetComponentCount;
if(element.position.logic instanceof Purchasable) {
if(element.position.logic instanceof Purchasable purchasable) {
targetComponentCount = 3;
if(element.owner != null) {
purchaseLabel.setText("Owner: " + element.owner.getIcon());
improvementLabel.setText(improvementString());
} else {
purchaseLabel.setText("Cost: " + purchasable.cost());
improvementLabel.setText(improvementString());
}
} else {
targetComponentCount = 1;
Expand All @@ -89,15 +88,14 @@ public boolean update(GameState gameState) {
}

for(Player player : gameState.getPlayers()) {
if(player.getPosition() == element.position) {
if(!player.isBankrupt() && player.getPosition() == element.position) {
GridBagConstraints constraints = new GridBagConstraints();
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.fill = GridBagConstraints.NONE;
constraints.anchor = GridBagConstraints.SOUTHWEST;
constraints.gridx = player.playerIndex;
constraints.gridy = targetComponentCount + 1;
playerVisuals.add(new PlayerVisual(player));
add(new PlayerVisual(player), constraints);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/xyz/cliserkad/consortium/BoardPosition.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum BoardPosition implements Serializable {
MEDITERRANEAN_AVENUE(new Color(0x8B4513), new StandardLogic(60, 50, new int[] { 2, 10, 30, 90, 160, 250 })),
COMMUNITY_CHEST_1(Color.WHITE, CommunityChestLogic.INSTANCE),
BALTIC_AVENUE(MEDITERRANEAN_AVENUE.color, new StandardLogic(60, 50, new int[] { 4, 20, 60, 180, 320, 450 })),
INCOME_TAX(Color.WHITE, TaxLogic.INCOME_TAX),
INCOME_TAX(Color.WHITE, IncomeTaxLogic.INSTANCE),
READING_RAILROAD(Color.BLACK, new RailRoadLogic()),
ORIENTAL_AVENUE(new Color(0xB2FFE8), new StandardLogic(100, 50, new int[] { 6, 30, 90, 270, 400, 550 })),
CHANCE_1(Color.WHITE, ChanceLogic.INSTANCE),
Expand Down Expand Up @@ -52,7 +52,7 @@ public enum BoardPosition implements Serializable {
SHORT_LINE(READING_RAILROAD.color, new RailRoadLogic()),
CHANCE_3(Color.WHITE, ChanceLogic.INSTANCE),
PARK_PLACE(new Color(0x1D1DE8), new StandardLogic(350, 200, new int[] { 35, 175, 500, 1100, 1300, 1500 })),
LUXURY_TAX(Color.WHITE, TaxLogic.LUXURY_TAX),
LUXURY_TAX(Color.WHITE, LuxuryTaxLogic.INSTANCE),
BOARDWALK(PARK_PLACE.color, new StandardLogic(400, 200, new int[] { 50, 200, 600, 1400, 1700, 2000 }));

@Serial
Expand Down
1 change: 1 addition & 0 deletions src/main/java/xyz/cliserkad/consortium/ChanceLogic.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public String onLand(Player player, GameState gameState) {
}
}
case BUILDING_LOAN -> player.addMoney(150);
// TODO: fix double auction failure when ADVANCE_TO_VERMONT is pulled
case ADVANCE_TO_VERMONT -> gameState.movePlayer(player, BoardPosition.VERMONT_AVENUE, 0);
}
return player.getIcon() + " pulled Chance Card \"" + pulledCard.niceName + "\"";
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/xyz/cliserkad/consortium/GameConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package xyz.cliserkad.consortium;

public class GameConfig {

public int initialMoney = 800;
public int minimumBid = 10;
public float startingBidFactor = 0.33f;

}
56 changes: 45 additions & 11 deletions src/main/java/xyz/cliserkad/consortium/GameServer.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package xyz.cliserkad.consortium;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import xyz.cliserkad.util.Text;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -15,10 +24,14 @@ public class GameServer {

private int turns;

public GameServer(final int playerCount) throws IOException, InterruptedException {
List<NetworkedController<GameClient>> controllers = new ArrayList<>();
public GameServer() throws IOException, InterruptedException {
GameConfig gameConfig = readConfigFile(new GameConfig(), GameConfig.class);
LobbyConfig lobbyConfig = readConfigFile(new LobbyConfig(), LobbyConfig.class);

for(int i = 0; i < playerCount; i++) {
System.out.println("Public IP: " + publicIP());

List<NetworkedController<GameClient>> controllers = new ArrayList<>();
for(int i = 0; i < lobbyConfig.networkClients; i++) {
controllers.add(new NetworkedController<>(BASE_PORT + i, GameClient.class, true));
}

Expand All @@ -32,22 +45,43 @@ public GameServer(final int playerCount) throws IOException, InterruptedExceptio

System.out.println("Connections accepted...");
List<GameClient> clients = new ArrayList<>();
for(int i = 0; i < lobbyConfig.autoClients; i++) {
clients.add(new AutoGameClient());
}
for(NetworkedController<GameClient> controller : controllers) {
clients.add(controller.proxy);
}
gameState = new GameState(clients);

gameState = new GameState(clients, gameConfig);
System.out.println("Game state initialized...");
}

public static void main(String[] args) throws IOException, InterruptedException {
final int playerCount;
public static String publicIP() {
try {
playerCount = Integer.parseInt(args[0]);
} catch(ArrayIndexOutOfBoundsException | NumberFormatException e) {
System.err.println("Failed to parse player count from command line arguments");
return;
URL amazonIP = new URI("http://checkip.amazonaws.com").toURL();
BufferedReader in = new BufferedReader(new InputStreamReader(amazonIP.openStream()));
return in.readLine();
} catch(Exception e) {
return null;
}
GameServer server = new GameServer(playerCount);
}

public static <T> T readConfigFile(T config, Class<T> configClass) {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String fileName = Text.undoCamelCase(config.getClass().getSimpleName()) + ".json";
try {
config = gson.fromJson(new FileReader(fileName), configClass);
} catch(IOException e) {
System.err.println(e.getMessage());
System.err.println("Failed to read " + fileName);
}
System.out.println("Current " + config.getClass().getSimpleName() + ":");
System.out.println(gson.toJson(config));
return config;
}

public static void main(String[] args) throws IOException, InterruptedException {
GameServer server = new GameServer();
while(server.run()) {
}
}
Expand Down
40 changes: 29 additions & 11 deletions src/main/java/xyz/cliserkad/consortium/GameState.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
public class GameState implements Serializable {

private final transient GameConfig config;
private final transient int[] communityCardStack = genShuffledArray(CommunityChestLogic.CommunityCard.values().length);
private final transient int[] chanceCardStack = genShuffledArray(ChanceLogic.ChanceCard.values().length);
private int communityCardIndex = 0;
Expand All @@ -38,7 +39,9 @@ public class GameState implements Serializable {
@Serial
private static final long serialVersionUID = 20240805L;

public GameState(List<GameClient> clients) {
public GameState(List<GameClient> clients, GameConfig config) {
this.config = config;

boardElements = new BoardElement[BoardPosition.values().length];
for(int i = 0; i < BoardPosition.values().length; i++) {
final BoardPosition position = BoardPosition.values()[i];
Expand All @@ -48,7 +51,7 @@ public GameState(List<GameClient> clients) {

players = new Player[clients.size()];
for(int i = 0; i < clients.size(); i++)
players[i] = new Player(clients.get(i));
players[i] = new Player(clients.get(i), config.initialMoney);

for(Player player : players) {
player.setPosition(BoardPosition.GO, this);
Expand All @@ -64,7 +67,7 @@ public GameState(List<GameClient> clients) {
* Creates a new game state with no players. Used as a placeholder for updating clients during game initialization.
*/
public GameState() {
this(new ArrayList<>());
this(new ArrayList<>(), new GameConfig());
}

/**
Expand Down Expand Up @@ -133,7 +136,10 @@ private void endOfTurnLoop() {
if(getCurrentPlayer().getMoney() < 0)
broadcast(getCurrentPlayer().getIcon() + " is in debt! (" + getCurrentPlayer().getMoney() + ") They will need to raise funds or declare bankruptcy.");
do {
response = updateAndPoll(getCurrentPlayer(), EndTurnAction.class);
if(getCurrentPlayer().getMoney() < 0)
response = updateAndPoll(getCurrentPlayer(), DeclareBankruptcyAction.class);
else
response = updateAndPoll(getCurrentPlayer(), EndTurnAction.class);
if(response instanceof ProposeTradeAction proposeTradeAction) {
broadcast("Trade proposed by " + proposeTradeAction.trade().proposer.getIcon() + " to " + proposeTradeAction.trade().acceptor.getIcon() + " " + proposeTradeAction.trade());
proposedTrade = proposeTradeAction.trade();
Expand All @@ -147,6 +153,13 @@ private void endOfTurnLoop() {
}
} else if(response instanceof DeclareBankruptcyAction) {
getCurrentPlayer().goBankrupt();
for(BoardElement element : boardElements) {
if(element.owner == getCurrentPlayer()) {
element.improvementAmt = 0;
element.setOwner(null);
}
}
updatePlayers();
broadcast(getCurrentPlayer().getIcon() + " has declared bankruptcy");
System.out.println("Players remaining: " + Arrays.toString(players));
return; // end the turn loop forcefully
Expand Down Expand Up @@ -244,34 +257,39 @@ private PlayerAction updateAndPoll(Player player, Class<? extends PlayerAction>
private String holdAuction(Player firstBidder, BoardElement element) {
if(element.position.logic instanceof Purchasable purchasable) {
// add all players who can afford the property to the auction
auction = new Auction(element, new ArrayList<>(Arrays.asList(players)));
auction.bidders.removeIf(player -> player.getMoney() < 1);
auction = new Auction(element, new ArrayList<>(Arrays.asList(players)), config);
auction.bidders.removeIf(player -> player.getMoney() < auction.bid + config.minimumBid);

// abort if no players have any money :(
if(auction.bidders.isEmpty())
return "No players have enough money to bid on " + element.position.niceName;

broadcast("Auction for " + element.position.niceName + " has begun! Starting bid: $" + (auction.bid + config.minimumBid));

// set the current bidder to the first bidder if they can participate
if(auction.bidders.contains(firstBidder))
auction.currentBidder = firstBidder;
else
auction.currentBidder = auction.bidders.getFirst();

while((auction.bidders.size() == 1 && auction.bid <= 0) || auction.bidders.size() > 1) {
while(auction.bidders.size() > 1 || (auction.bidders.size() == 1 && auction.bids.isEmpty())) {
// save for later :)
final Player nextBidder = auction.bidders.get((auction.bidders.indexOf(auction.currentBidder) + 1) % auction.bidders.size());
PlayerAction response = updateAndPoll(auction.currentBidder, BidAction.class);
if(response instanceof BidAction bidAction && bidAction.amount() > auction.bid && auction.currentBidder.getMoney() >= bidAction.amount()) {
if(response instanceof BidAction bidAction && bidAction.amount() >= auction.bid + config.minimumBid && auction.currentBidder.getMoney() >= bidAction.amount()) {
auction.bid = bidAction.amount();
auction.bids.add(bidAction);
broadcast(auction.currentBidder.getIcon() + " bid $" + auction.bid + " on " + element.position.niceName);
} else {
auction.bidders.remove(auction.currentBidder);
broadcast(auction.currentBidder.getIcon() + " withdrew from bidding on " + element.position.niceName);
}
if(!auction.bidders.isEmpty())
auction.currentBidder = auction.bidders.get((auction.currentBidder.playerIndex + 1) % auction.bidders.size());
// this is later
auction.currentBidder = nextBidder;
}

if(!auction.bidders.isEmpty() && auction.bid > 0) {
// if any bids were placed, the property is sold to the last remaining bidder
if(!auction.bids.isEmpty() && !auction.bidders.isEmpty()) {
final Player auctionWinner = auction.bidders.getFirst();
auctionWinner.addMoney(-auction.bid);
element.setOwner(auctionWinner);
Expand Down
Loading