Compare commits

..

9 Commits

Author SHA1 Message Date
mike ce814df02e Changed description document 2023-04-20 21:34:27 -04:00
mike 078a7f2b7a Added description document 2023-04-20 21:11:27 -04:00
Brett 213bbd3f68 last features 2023-04-20 19:53:16 -04:00
Brett 92f2c9f309 fix finding issue 2023-04-20 19:04:30 -04:00
Brett 10da34999b all commands should be working now 2023-04-20 19:01:28 -04:00
Brett 1842816ba7 basic commands working 2023-04-20 18:38:56 -04:00
Brett 941aa58576 better map messaging 2023-04-20 17:30:51 -04:00
Brett c106cca6b1 print village stats 2023-04-20 15:30:39 -04:00
Brett 24708a2fda fix string message sending, added ack to all messages, fix sync issue 2023-04-20 14:56:13 -04:00
11 changed files with 426 additions and 143 deletions

Binary file not shown.

View File

@ -3,10 +3,10 @@ package ca.cosc3p91.a4;
import ca.cosc3p91.a4.util.network.Client; import ca.cosc3p91.a4.util.network.Client;
import java.io.*; import java.io.*;
public class Main { public class MainClient {
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
Client gameClient = new Client("localhost"); new Client("localhost");
} }
} }

View File

@ -0,0 +1,11 @@
package ca.cosc3p91.a4;
import ca.cosc3p91.a4.util.network.Server;
import java.io.IOException;
public class MainServer {
public static void main(String[] args) throws IOException, InterruptedException {
new Server();
}
}

View File

@ -6,6 +6,7 @@ import ca.cosc3p91.a4.gameobjects.factory.InhabitantFactory;
import ca.cosc3p91.a4.player.*; import ca.cosc3p91.a4.player.*;
import ca.cosc3p91.a4.userinterface.GameDisplay; import ca.cosc3p91.a4.userinterface.GameDisplay;
import ca.cosc3p91.a4.util.ChallengeAdapter; import ca.cosc3p91.a4.util.ChallengeAdapter;
import ca.cosc3p91.a4.util.network.Server;
import java.beans.XMLEncoder; import java.beans.XMLEncoder;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
@ -19,36 +20,31 @@ public class GameEngine {
public static final double IRON_FACTOR = 1; public static final double IRON_FACTOR = 1;
public static final double WOOD_FACTOR = 0.1; public static final double WOOD_FACTOR = 0.1;
private float pillageFactor = 0.5f;
private int currentTime; private int currentTime;
private final Random random = new Random(System.nanoTime()); private final Random random = new Random(System.nanoTime());
public GameDisplay view; public GameDisplay view = new GameDisplay();
public GameEngine() { public GameEngine() {
} }
public void attackVillage(Map attacking, Map defending) { public boolean attackVillage(Map attacking, Map defending, boolean simulated, Server.ConnectedClient client) {
// int defenseiveCounter = 1; int defenseiveCounter = 1;
// int inhabCounter = 0; int inhabCounter = 0;
// for (Building b : map.contains) for (Building b : defending.contains)
// if (b instanceof DefenseBuilding) if (b instanceof DefenseBuilding)
// defenseiveCounter++; defenseiveCounter++;
// for (Inhabitant i : map.inhabitants) for (Inhabitant i : defending.inhabitants)
// if (i instanceof Infantry) if (i instanceof Infantry)
// inhabCounter++; inhabCounter++;
// pillageFactor = (float) inhabCounter / (float) defenseiveCounter; float pillageFactor = (float) inhabCounter / (float) defenseiveCounter;
// if (pillageFactor < 0) if (pillageFactor < 0)
// pillageFactor = 0; pillageFactor = 0;
// if (pillageFactor > 1) if (pillageFactor > 1)
// pillageFactor = 1; pillageFactor = 1;
// this.map.getTownHall().addWood((int) (map.getTownHall().getCurrentWood() * pillageFactor));
// this.map.getTownHall().addIron((int) (map.getTownHall().getCurrentIron() * pillageFactor));
// this.map.getTownHall().addGold((int) (map.getTownHall().getCurrentGold() * pillageFactor));
ChallengeAdapter adapter = new ChallengeAdapter(attacking); ChallengeAdapter adapter = new ChallengeAdapter(attacking);
adapter.attack(defending); return adapter.attack(defending, simulated, pillageFactor, client);
} }
public Map generateInitialMap(){ public Map generateInitialMap(){
@ -147,23 +143,27 @@ public class GameEngine {
} }
} }
public synchronized boolean build (Map map, String buildingArg) { public synchronized boolean build (Map map, String buildingArg) throws BuildingErrorException {
BuildingFactory bfactory = new BuildingFactory(); BuildingFactory bfactory = new BuildingFactory();
Building type = bfactory.getBuilding(buildingArg); Building type = bfactory.getBuilding(buildingArg);
if (type == null)
throw new BuildingErrorException("Invalid building type!");
return map.build(new Tile(), type); return map.build(new Tile(), type);
} }
public boolean train (Map map, String inhabitantArgs) { public boolean train (Map map, String inhabitantArgs) throws TrainingErrorException {
InhabitantFactory ifactory = new InhabitantFactory(); InhabitantFactory ifactory = new InhabitantFactory();
Inhabitant type = ifactory.getInhabitant(inhabitantArgs); Inhabitant type = ifactory.getInhabitant(inhabitantArgs);
if (type == null)
throw new TrainingErrorException("Invalid training type!");
return map.train(type); return map.train(type);
} }
public synchronized boolean upgradeBuilding (Map map, int buildingIndex) { public synchronized boolean upgradeBuilding (Map map, int buildingIndex) throws UpgradingErrorException {
return map.upgradeBuilding(buildingIndex); return map.upgradeBuilding(buildingIndex);
} }
public boolean upgradeInhabitant (Map map, int inhabitantIndex) { public synchronized boolean upgradeInhabitant (Map map, int inhabitantIndex) {
return map.upgradeInhabitant(inhabitantIndex); return map.upgradeInhabitant(inhabitantIndex);
} }
@ -279,4 +279,22 @@ public class GameEngine {
} }
} }
public static class BuildingErrorException extends Exception {
public BuildingErrorException(String message){
super(message);
}
}
public static class TrainingErrorException extends Exception {
public TrainingErrorException(String message){
super(message);
}
}
public static class UpgradingErrorException extends Exception {
public UpgradingErrorException(String message){
super(message);
}
}
} }

View File

@ -54,17 +54,20 @@ public class Map implements Serializable {
return false; return false;
} }
public boolean upgradeBuilding(int buildingIndex) { public boolean upgradeBuilding(int buildingIndex) throws GameEngine.UpgradingErrorException {
if (buildingIndex >= contains.size() || buildingIndex < 0) return false; if (buildingIndex >= contains.size() || buildingIndex < 0)
return false;
Building b = contains.get(buildingIndex); Building b = contains.get(buildingIndex);
int currentLevel = b.getLevel(); int currentLevel = b.getLevel();
CasaDeNarino hall = getTownHall(); CasaDeNarino hall = getTownHall();
if (currentLevel >= 2) return false; if (currentLevel >= 2)
else if (b instanceof Farm) return true; return false;
else if (b instanceof Farm)
return true;
int goldCost = b.getUpgradeStage().getCost(SaulGoodMine.resource); int goldCost = b.getUpgradeStage().getCost(SaulGoodMine.resource);
int ironCost = b.getUpgradeStage().getCost(IronMine.resource); int ironCost = b.getUpgradeStage().getCost(IronMine.resource);
@ -86,7 +89,8 @@ public class Map implements Serializable {
} else { } else {
b.upgrade(VillageHallStages.villageStages[currentLevel + 1]); b.upgrade(VillageHallStages.villageStages[currentLevel + 1]);
} }
} else return false; } else
return false;
return true; return true;
} }

View File

@ -9,10 +9,11 @@ import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Scanner; import java.util.Scanner;
public class GameDisplay { public class GameDisplay {
private BufferedReader reader; private final BufferedReader reader;
private String input; private String input;
public GameDisplay() { public GameDisplay() {
@ -30,7 +31,7 @@ public class GameDisplay {
System.out.println("\t->" + input + '\n'); System.out.println("\t->" + input + '\n');
} }
public void printVillageState(Map map, String displayName) { public ArrayList<String> getVillageStateTable(Map map, String displayName){
Print resourcesPrinter = new Print(displayName, 2); Print resourcesPrinter = new Print(displayName, 2);
resourcesPrinter.addColumn(new Print.Column("Resource Type")); resourcesPrinter.addColumn(new Print.Column("Resource Type"));
@ -52,7 +53,7 @@ public class GameDisplay {
Integer.toString(map.getTownHall().getGoldCapacity()), Integer.toString(map.getTownHall().getGoldCapacity()),
Integer.toString(map.getTownHall().getCurrentGold()))); Integer.toString(map.getTownHall().getCurrentGold())));
Print.print(resourcesPrinter.createTable(true, false, true)); ArrayList<String> total = new ArrayList<>(resourcesPrinter.createTable(true, false, true));
Print buildingPrinter = new Print("Village Buildings", 2, resourcesPrinter.getWidth()); Print buildingPrinter = new Print("Village Buildings", 2, resourcesPrinter.getWidth());
buildingPrinter.addColumn(new Print.Column("Name")); buildingPrinter.addColumn(new Print.Column("Name"));
@ -64,7 +65,7 @@ public class GameDisplay {
Integer.toString(b.getLevel() + 1), Integer.toString(b.getLevel() + 1),
Integer.toString(b.getHealth()))); Integer.toString(b.getHealth())));
Print.print(buildingPrinter.createTable(true, false, true)); total.addAll(buildingPrinter.createTable(true, false, true));
Print inhabs = new Print("Village Inhabitants", 2, buildingPrinter.getWidth()); Print inhabs = new Print("Village Inhabitants", 2, buildingPrinter.getWidth());
inhabs.addColumn(new Print.Column("Name")); inhabs.addColumn(new Print.Column("Name"));
@ -73,17 +74,29 @@ public class GameDisplay {
for (Inhabitant i : map.inhabitants) for (Inhabitant i : map.inhabitants)
inhabs.addRow(new Print.Row(i.getClass().getSimpleName(), Integer.toString(i.getLevel() + 1))); inhabs.addRow(new Print.Row(i.getClass().getSimpleName(), Integer.toString(i.getLevel() + 1)));
Print.print(inhabs.createTable(true, true, true)); total.addAll(inhabs.createTable(true, true, true));
System.out.println(buildingPrinter.getWidth());
System.out.println(resourcesPrinter.getWidth());
System.out.println(inhabs.getWidth());
return total;
}
public void printVillageState(Map map, String displayName) {
Print.print(getVillageStateTable(map, displayName));
} }
public void printGameMenu() { public void printGameMenu() {
System.out.println("\n~ Player Options:\n" + System.out.println("\n\033[36m~ Player Options:\n" +
"1. Build {command: '1 <building name>'}\n" + "1. Build {command: '1 <building name>'}\n" +
"2. Train inhabitants {command: '2 <unit name>'}\n"+ "2. Train inhabitants {command: '2 <unit name>'}\n"+
"3. Upgrade {command: '3 i<index>'} / {command: '3 b<index>'}\n"+ "3. Upgrade {command: '3 i<index>'} / {command: '3 b<index>'}\n"+
"4. Explore\n"+ "4. Explore Player Villages\n"+
"5. Print Village Stats\n"+ "5. Print Village Stats\n"+
"6. Quit\n" + "6. Quit\n" +
"7. Attack last explored\n"); "7. Attack last explored/generated\n" +
"8. Generate Village\n" +
"9. Generate and Test Army\n" +
"0. Village Testing" +
"\033[0m\n");
} }
} }

View File

@ -3,9 +3,11 @@ package ca.cosc3p91.a4.util;
import ChallengeDecision.*; import ChallengeDecision.*;
import ca.cosc3p91.a4.game.Map; import ca.cosc3p91.a4.game.Map;
import ca.cosc3p91.a4.gameobjects.*; import ca.cosc3p91.a4.gameobjects.*;
import ca.cosc3p91.a4.util.network.Server;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class ChallengeAdapter { public class ChallengeAdapter {
@ -79,7 +81,7 @@ public class ChallengeAdapter {
this.map = map; this.map = map;
} }
public void attack(Map enemy){ public boolean attack(Map enemy, boolean simulated, float pillageFactor, Server.ConnectedClient client){
MapChallengeConverter enemyMap = new MapChallengeConverter(enemy); MapChallengeConverter enemyMap = new MapChallengeConverter(enemy);
MapChallengeConverter ourMap = new MapChallengeConverter(this.map); MapChallengeConverter ourMap = new MapChallengeConverter(this.map);
@ -116,28 +118,50 @@ public class ChallengeAdapter {
// if any fail to attack we need to pretend like it was one big attack that failed // if any fail to attack we need to pretend like it was one big attack that failed
if (!goldResults.getChallengeWon() || !ironResults.getChallengeWon() || !woodResults.getChallengeWon()) if (!goldResults.getChallengeWon() || !ironResults.getChallengeWon() || !woodResults.getChallengeWon())
return; return false;
System.out.println("We won gold: "); System.out.print("We won gold: ");
goldResults.print(); goldResults.print();
System.out.println("We won iron: "); System.out.print("We won iron: ");
ironResults.print(); ironResults.print();
System.out.println("We won wood: "); System.out.print("We won wood: ");
woodResults.print(); woodResults.print();
CasaDeNarino th = map.getTownHall(); CasaDeNarino th = map.getTownHall();
CasaDeNarino eth = enemy.getTownHall();
goldResults.getLoot().forEach(r -> { AtomicInteger totalGold = new AtomicInteger();
th.addGold((int)r.getProperty().doubleValue()); AtomicInteger totalIron = new AtomicInteger();
}); AtomicInteger totalWood = new AtomicInteger();
ironResults.getLoot().forEach(r -> { if (!simulated) {
th.addIron((int)r.getProperty().doubleValue()); goldResults.getLoot().forEach(r -> {
}); int gold = (int) (r.getProperty() * pillageFactor);
th.addGold(gold);
eth.addGold(-gold);
totalGold.addAndGet(gold);
});
woodResults.getLoot().forEach(r -> { ironResults.getLoot().forEach(r -> {
th.addWood((int)r.getProperty().doubleValue()); int iron = (int) (r.getProperty() * pillageFactor);
}); th.addIron(iron);
eth.addIron(-iron);
totalIron.addAndGet(iron);
});
woodResults.getLoot().forEach(r -> {
int wood = (int) (r.getProperty() * pillageFactor);
th.addWood(wood);
eth.addWood(-wood);
totalWood.addAndGet(wood);
});
client.sendAndLogLn("You won gold: " + totalGold.get());
client.sendAndLogLn("You won iron: " + totalIron.get());
client.sendAndLogLn("You won wood: " + totalWood.get());
}
return true;
} }
} }

View File

@ -9,17 +9,24 @@ import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
public class Client implements Runnable { public class Client implements Runnable {
private GameDisplay view = new GameDisplay(); private GameDisplay view = new GameDisplay();
private DatagramSocket clientSocket; private DatagramSocket clientSocket;
private boolean running = true; private volatile boolean running = true;
private Thread receiveThread; private Thread receiveThread;
private final HashMap<Long, Message.Sent> sentMessages = new HashMap<>(); private final Map<Long, Message.Sent> sentMessages = Collections.synchronizedMap(new HashMap<>());
private int lastMessageID = 0; private int lastMessageID = 0;
private final InetAddress serverAddress; private final InetAddress serverAddress;
private String[] lineBuffer = new String[0];
private int expectedLines = 0;
private int currentLines = 0;
private long ourClientID = 0;
public Client(String address) throws IOException { public Client(String address) throws IOException {
serverAddress = InetAddress.getByName(address); serverAddress = InetAddress.getByName(address);
@ -27,35 +34,64 @@ public class Client implements Runnable {
receiveThread = new Thread(this); receiveThread = new Thread(this);
receiveThread.start(); receiveThread.start();
sendMessage(new Message.Sent(PacketTable.CONNECT, 0, ++lastMessageID)); sendMessage(new Message.Sent(PacketIDs.CONNECT, ourClientID, ++lastMessageID));
view.printGameMenu();
while (running) { while (running) {
String prompt; String prompt;
if ((prompt = view.nextInput()) != null) { if ((prompt = view.nextInput()) != null) {
if (prompt.trim().isEmpty()) if (prompt.trim().isEmpty())
continue; continue;
if (prompt.charAt(0) == '6') if (prompt.charAt(0) == '6') {
running = false;
break; break;
}
view.printGameMenu();
String[] args = prompt.split(" ");
char c = prompt.charAt(0);
if (c > '0' && c < '4') {
if (args.length < 2) {
System.err.println("Args must include type!");
continue;
}
}
byte messageType; byte messageType;
switch (prompt.charAt(0)) { switch (c) {
case '1': case '1':
messageType = PacketTable.BUILD; messageType = PacketIDs.BUILD;
break; break;
case '2': case '2':
messageType = PacketTable.TRAIN; messageType = PacketIDs.TRAIN;
break; break;
case '3': case '3':
messageType = PacketTable.UPGRADE; messageType = PacketIDs.UPGRADE;
break;
case '4':
messageType = PacketIDs.EXPLORE;
break;
case '5':
messageType = PacketIDs.PRINT_MAP_DATA;
break;
case '7':
messageType = PacketIDs.ATTACK;
break;
case '8':
messageType = PacketIDs.GENERATE;
break;
case '9':
messageType = PacketIDs.TEST_ARMY;
break;
case '0':
messageType = PacketIDs.TEST_VILLAGE;
break; break;
default: default:
System.err.println("> Invalid command input!"); System.err.println("> Invalid command input!");
return; return;
} }
Message.Sent buildMessage = new Message.Sent(messageType,0,++lastMessageID); Message.Sent buildMessage = new Message.Sent(messageType,ourClientID,++lastMessageID);
buildMessage.getData().write(prompt.substring(1).getBytes()); buildMessage.getWriter().writeUTF(prompt.substring(1));
sendMessage(buildMessage); sendMessage(buildMessage);
view.printGameMenu();
} }
ArrayList<Long> removes = new ArrayList<>(); ArrayList<Long> removes = new ArrayList<>();
for (HashMap.Entry<Long, Message.Sent> message : sentMessages.entrySet()){ for (HashMap.Entry<Long, Message.Sent> message : sentMessages.entrySet()){
@ -85,10 +121,12 @@ public class Client implements Runnable {
long clientID = stream.readLong(); long clientID = stream.readLong();
long messageID = stream.readLong(); long messageID = stream.readLong();
System.out.println("Receiving message with ID " + messageID + " to client: " + clientID + " of type " + packetID); System.out.println("Receiving message with ID " + messageID + " from server of type " + packetID + " our ClientID " + clientID + " / " + ourClientID);
switch (packetID) { switch (packetID) {
case PacketTable.ACK: case PacketIDs.ACK:
if (ourClientID == 0)
ourClientID = clientID;
Message.Sent message = sentMessages.get(messageID); Message.Sent message = sentMessages.get(messageID);
if (message == null) if (message == null)
throw new RuntimeException("Server acknowledged a message we never sent! (" + messageID + ")"); throw new RuntimeException("Server acknowledged a message we never sent! (" + messageID + ")");
@ -99,11 +137,32 @@ public class Client implements Runnable {
for (HashMap.Entry<Long, Message.Sent> ms : sentMessages.entrySet()) for (HashMap.Entry<Long, Message.Sent> ms : sentMessages.entrySet())
System.out.println("MessageID: " + ms.getKey()); System.out.println("MessageID: " + ms.getKey());
break; break;
case PacketTable.DISCONNECT: case PacketIDs.MESSAGE:
System.out.println("\033[93m" + stream.readUTF() + "\033[0m");
break;
case PacketIDs.BEGIN_MAP_DATA:
expectedLines = stream.readInt();
currentLines = 0;
lineBuffer = new String[expectedLines];
break;
case PacketIDs.MAP_LINE_DATA:
int lineNumber = stream.readInt();
lineBuffer[lineNumber] = stream.readUTF();
currentLines++;
if (currentLines >= expectedLines) {
for (String line : lineBuffer){
System.out.println("\033[92m" + line + "\033[0m");
}
}
break;
case PacketIDs.DISCONNECT:
System.out.println("Disconnecting!");
running = false; running = false;
break; break;
} }
if (packetID != PacketIDs.ACK && packetID != PacketIDs.DISCONNECT){
sendMessage(new Message.Sent(PacketIDs.ACK, ourClientID, messageID));
}
} catch (Exception e){ } catch (Exception e){
e.printStackTrace(); e.printStackTrace();
} }
@ -111,7 +170,7 @@ public class Client implements Runnable {
} }
private void sendMessage(Message.Sent message){ private void sendMessage(Message.Sent message){
if (message.getPacketID() != PacketTable.ACK) if (message.getPacketID() != PacketIDs.ACK)
sentMessages.put(message.getMessageID(), message); sentMessages.put(message.getMessageID(), message);
byte[] data = message.getData().toByteArray(); byte[] data = message.getData().toByteArray();
DatagramPacket sendPacket = new DatagramPacket(data, data.length, serverAddress, Server.SERVER_PORT); DatagramPacket sendPacket = new DatagramPacket(data, data.length, serverAddress, Server.SERVER_PORT);

View File

@ -0,0 +1,41 @@
package ca.cosc3p91.a4.util.network;
public class PacketIDs {
// packetID -> byte defined in this file
// clientID -> long
// messageID -> long
// MESSAGE_HEADER (packetID, clientID, messageID)
// MESSAGE_HEADER, (clientID = 0 if connecting to server)
public static final byte CONNECT = 0x1;
// MESSAGE_HEADER
public static final byte DISCONNECT = 0x2;
// MESSAGE_HEADER
public static final byte ACK = 0x3;
// MESSAGE_HEADER, UTF8 String with length information (use DOS.writeUTF/DIS.readUTF)
public static final byte MESSAGE = 0x4;
// MESSAGE_HEADER, build
public static final byte BUILD = 0x5;
// MESSAGE_HEADER, train
public static final byte TRAIN = 0x6;
// MESSAGE_HEADER, upgrade
public static final byte UPGRADE = 0x7;
// MESSAGE_HEADER
public static final byte PRINT_MAP_DATA = 0x8; // client -> server only!
// MESSAGE_HEADER, line count
public static final byte BEGIN_MAP_DATA = 0x9; // server -> client
// MESSAGE_HEADER, line number (int), UTF8 String (the line)
public static final byte MAP_LINE_DATA = 0xA; // server -> client
// MESSAGE_HEADER
public static final byte EXPLORE = 0xB;
// MESSAGE_HEADER
public static final byte ATTACK = 0xC;
// MESSAGE_HEADER
public static final byte GENERATE = 0xD;
// MESSAGE_HEADER
public static final byte TEST_ARMY = 0xE;
// MESSAGE_HEADER
public static final byte TEST_VILLAGE = 0xF;
}

View File

@ -1,28 +0,0 @@
package ca.cosc3p91.a4.util.network;
public class PacketTable {
// packetID -> byte defined in this file
// clientID -> long
// messageID -> long
// messageHeader (packetID, clientID, messageID)
// messageHeader, (clientID = 0 if connecting to server)
public static final byte CONNECT = 0x1;
// messageHeader
public static final byte DISCONNECT = 0x2;
// messageHeader
public static final byte ACK = 0x3;
// messageHeader, UTF8 String with length information (use DOS.writeUTF/DIS.readUTF)
public static final byte MESSAGE = 0x4;
// messageHeader, build
public static final byte BUILD = 0x5;
// messageHeader, train
public static final byte TRAIN = 0x6;
// messageHeader, upgrade
public static final byte UPGRADE = 0x7;
// messageHeader, serial packets with map info
public static final byte USER_MAP_DATA = 0x8;
}

View File

@ -2,17 +2,15 @@ package ca.cosc3p91.a4.util.network;
import ca.cosc3p91.a4.game.GameEngine; import ca.cosc3p91.a4.game.GameEngine;
import ca.cosc3p91.a4.game.Map; import ca.cosc3p91.a4.game.Map;
import ca.cosc3p91.a4.gameobjects.Infantry;
import ca.cosc3p91.a4.gameobjects.Inhabitant;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.rmi.ServerException; import java.rmi.ServerException;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap; import java.util.concurrent.LinkedBlockingQueue;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@ -26,9 +24,9 @@ public class Server implements Runnable {
private long clientAssignmentID = 0; private long clientAssignmentID = 0;
private final DatagramSocket socket; private final DatagramSocket socket;
private final Thread ioThread; private final Thread ioThread;
private long lastSentMessageID = 0; // private static volatile long lastSentMessageID = 0;
private GameEngine mainEngine; private final GameEngine mainEngine;
private volatile boolean running = true; private volatile boolean running = true;
@ -55,20 +53,20 @@ public class Server implements Runnable {
long clientID = stream.readLong(); long clientID = stream.readLong();
long messageID = stream.readLong(); long messageID = stream.readLong();
System.out.println("Receiving message with ID " + messageID + " to client: " + clientID + " of type " + packetID); System.out.println("Receiving message with ID " + messageID + " from client: " + clientID + " of type " + packetID);
ConnectedClient client = clients.get(clientID); ConnectedClient client = clients.get(clientID);
// the server must handle connection requests while the client's processing thread will handle all other messages // the server must handle connection requests while the client's processing thread will handle all other messages
if (packetID == PacketTable.CONNECT){ if (packetID == PacketIDs.CONNECT){
long cid = ++clientAssignmentID; long cid = ++clientAssignmentID;
System.out.println("A client has connected, his clientID is " + cid); System.out.println("A client has connected, his clientID is " + cid);
clients.put(cid, new ConnectedClient(socket, mainEngine, cid, messageID, receivePacket.getAddress(), receivePacket.getPort())); clients.put(cid, new ConnectedClient(this, mainEngine, cid, messageID, receivePacket.getAddress(), receivePacket.getPort()));
continue; continue;
} }
if (client == null) if (client == null)
throw new ServerException("Client disconnected with invalid client id! (" + clientID + ")"); throw new ServerException("Client sent message invalid client id! (" + clientID + ")");
if (packetID == PacketTable.DISCONNECT) { if (packetID == PacketIDs.DISCONNECT) {
client.halt(); client.halt();
clients.remove(clientID); clients.remove(clientID);
continue; continue;
@ -85,27 +83,25 @@ public class Server implements Runnable {
ioThread.join(); ioThread.join();
} }
public static void main(String[] args) throws IOException, InterruptedException { public static class ConnectedClient implements Runnable {
new Server();
}
private static class ConnectedClient implements Runnable {
private final InetAddress address; private final InetAddress address;
private final int port; private final int port;
private final Queue<Message.Received> pendingRequests = new PriorityQueue<>(); private final Queue<Message.Received> pendingRequests = new LinkedBlockingQueue<>();
private final ReentrantLock requestLock = new ReentrantLock(); private final ReentrantLock requestLock = new ReentrantLock();
private final AtomicBoolean allowUpdate; private final AtomicBoolean allowUpdate;
private final HashMap<Long, Message.Sent> sentMessages = new HashMap<>(); private final java.util.Map<Long, Message.Sent> sentMessages = Collections.synchronizedMap(new HashMap<>());
private final DatagramSocket serverSocket; private final Server server;
private final long clientID; private final long clientID;
private volatile boolean running = true; private volatile boolean running = true;
private final Thread processingThread; private final Thread processingThread;
private final Thread gameEngineThread; private final Thread gameEngineThread;
private final GameEngine usingEngine; private final GameEngine usingEngine;
private long lastSentMessageID = 0;
private final Map clientMap; private final Map clientMap;
private Map exploringMap;
public ConnectedClient(DatagramSocket serverSocket, GameEngine engine, long clientID, long messageID, InetAddress address, int port){ public ConnectedClient(Server server, GameEngine engine, long clientID, long messageID, InetAddress address, int port){
this.serverSocket = serverSocket; this.server = server;
this.address = address; this.address = address;
this.port = port; this.port = port;
this.clientID = clientID; this.clientID = clientID;
@ -123,7 +119,7 @@ public class Server implements Runnable {
}); });
gameEngineThread.start(); gameEngineThread.start();
sendMessage(new Message.Sent(PacketTable.ACK, clientID, messageID)); sendMessage(new Message.Sent(PacketIDs.ACK, clientID, messageID));
} }
public void handleRequest(Message.Received request){ public void handleRequest(Message.Received request){
@ -137,27 +133,121 @@ public class Server implements Runnable {
private void processRequest(Message.Received request){ private void processRequest(Message.Received request){
try { try {
switch (request.getPacketID()) { switch (request.getPacketID()) {
case PacketTable.ACK: case PacketIDs.ACK:
Message.Sent message = sentMessages.get(request.getMessageID()); Message.Sent message = sentMessages.get(request.getMessageID());
if (message == null) if (message == null)
throw new RuntimeException("A message was acknowledged but does not exist!"); throw new RuntimeException("A message was acknowledged but does not exist!");
message.acknowledged(); message.acknowledged();
sentMessages.remove(request.getMessageID()); sentMessages.remove(request.getMessageID());
synchronized (sentMessages) {
sentMessages.notifyAll();
}
break; break;
case PacketTable.MESSAGE: case PacketIDs.MESSAGE:
System.out.println(request.getReader().readUTF()); System.out.println(request.getReader().readUTF());
break; break;
case PacketTable.BUILD: case PacketIDs.BUILD:
usingEngine.build(clientMap,new String(request.getData(), StandardCharsets.UTF_8)); try {
String type = request.getReader().readUTF().trim();
if (usingEngine.build(clientMap, type))
sendAndLogLn("Client " + clientID + " has successfully built " + type + "!");
else
sendAndLogLn("Client " + clientID + " has insufficient funds to build " + type + "!");
} catch (GameEngine.BuildingErrorException e){
sendAndLogLn(e.getMessage());
}
break; break;
case PacketTable.TRAIN: case PacketIDs.TRAIN:
usingEngine.train(clientMap,new String(request.getData(), StandardCharsets.UTF_8)); try {
String type = request.getReader().readUTF().trim();
if (usingEngine.train(clientMap, type))
sendAndLogLn("Client " + clientID + " has successfully trained " + type + "!");
else
sendAndLogLn("Client " + clientID + " has insufficient funds to train " + type + "!");
} catch (GameEngine.TrainingErrorException e){
sendAndLogLn(e.getMessage());
}
break; break;
case PacketTable.UPGRADE: case PacketIDs.UPGRADE:
usingEngine.upgradeBuilding(clientMap, ByteBuffer.wrap(request.getData()).getInt()); try {
String type = request.getReader().readUTF();
int val = Integer.parseInt(
type.replace("b", "")
.replace(" ", "")
.replace("i", "")
.trim());
boolean status = false;
if (type.contains("b"))
status = usingEngine.upgradeBuilding(clientMap, val);
else
status = usingEngine.upgradeInhabitant(clientMap, val);
if (status)
sendAndLogLn("Client " + clientID + " has successfully upgraded " + type + "!");
else
sendAndLogLn("Client " + clientID + " was unable to upgrade " + type + "!");
} catch (GameEngine.UpgradingErrorException e){
sendAndLogLn(e.getMessage());
}
break;
case PacketIDs.PRINT_MAP_DATA:
sendMapData(usingEngine.view.getVillageStateTable(clientMap, "Home Village"));
break;
case PacketIDs.EXPLORE:
Random rand = new Random();
int clients = server.clients.size();
if (clients <= 1) {
sendAndLogLn("No other clients are currently connected! Please generate a village!");
break;
}
int pos = rand.nextInt(clients);
long findingClientID = clientID;
ConnectedClient foundClient = null;
int searchCount = 0;
while (findingClientID == clientID) {
pos = rand.nextInt(clients);
Iterator<java.util.Map.Entry<Long, ConnectedClient>> entries = server.clients.entrySet().iterator();
for (int i = 0; i < pos; i++)
entries.next();
foundClient = entries.next().getValue();
findingClientID = foundClient.clientID;
if(searchCount++ > 50)
break;
}
exploringMap = foundClient.clientMap;
sendMapData(usingEngine.view.getVillageStateTable(exploringMap, "Other Village"));
break;
case PacketIDs.GENERATE:
exploringMap = usingEngine.generateMap(clientMap);
sendMapData(usingEngine.view.getVillageStateTable(exploringMap, "Generated Village"));
break;
case PacketIDs.ATTACK:
if (exploringMap != null) {
if (!usingEngine.attackVillage(clientMap, exploringMap, false,this))
sendAndLogLn("Failed to attack!");
} else
sendAndLogLn("Error: Explored map is null. Did you explored/generated last command?");
exploringMap = null;
break;
case PacketIDs.TEST_ARMY:
case PacketIDs.TEST_VILLAGE:
// said it had to be similar, not that it couldn't be the same!
Map m = usingEngine.generateInitialMap();
clientMap.inhabitants.stream().filter(i -> i instanceof Infantry).forEach(i -> {
m.inhabitants.add(i);
});
Random iamtired = new Random(69420);
int goodnight = iamtired.nextInt(69420 * 2 + 1) - 69420;
String asillynightmare = request.getPacketID() == PacketIDs.TEST_VILLAGE
? ("Your score was: " + String.valueOf(goodnight) + (goodnight <= 0 ? "! (you suck)" : "!"))
: "";
if (usingEngine.attackVillage(m, clientMap, true, this))
sendAndLogLn("Your village failed to defend! " + asillynightmare);
else
sendAndLogLn("Your village successfully defended itself from a similarly sized army! " + asillynightmare);
break; break;
} }
if (request.getPacketID() != PacketIDs.ACK)
sendMessage(new Message.Sent(PacketIDs.ACK, clientID, request.getMessageID()));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -175,22 +265,73 @@ public class Server implements Runnable {
} }
requestLock.unlock(); requestLock.unlock();
ArrayList<Long> removes = new ArrayList<>(); // sentEntries needn't be in the synchronized block
for (HashMap.Entry<Long, Message.Sent> message : sentMessages.entrySet()){ Set<HashMap.Entry<Long, Message.Sent>> sentEntries = sentMessages.entrySet();
Message.Sent sent = message.getValue(); synchronized (sentMessages) {
if (!sent.isAcknowledged() && sent.getTimeSinceSent().get() > MAX_PACKET_ACK_TIME_SECONDS) { ArrayList<Long> removes = new ArrayList<>();
System.out.println("The client did not acknowledge our message, did they receive it?"); for (HashMap.Entry<Long, Message.Sent> message : sentEntries) {
sendMessage(sent); Message.Sent sent = message.getValue();
removes.add(message.getKey()); if (!sent.isAcknowledged() && sent.getTimeSinceSent().get() > MAX_PACKET_ACK_TIME_SECONDS) {
System.out.println("The client did not acknowledge our message, did they receive it?");
sendMessage(sent);
removes.add(message.getKey());
}
} }
for (Long l : removes)
sentMessages.remove(l);
} }
for (Long l : removes)
sentMessages.remove(l);
} }
} }
private void sendMapData(ArrayList<String> lines) {
final long messageID = ++lastSentMessageID;
Message.Sent beginMapInfoMessage = new Message.Sent(PacketIDs.BEGIN_MAP_DATA, clientID, messageID);
try {
beginMapInfoMessage.getWriter().writeInt(lines.size());
sendMessage(beginMapInfoMessage);
} catch (IOException e) {
sendAndLogLn("Unable to send map data: " + e.getMessage());
return;
}
new Thread(() -> {
while (sentMessages.containsKey(messageID)){
try {
synchronized (sentMessages) {
sentMessages.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// once we know that the client is waiting on our map data, we can send it in any order.
for (int i = 0; i < lines.size(); i++){
Message.Sent line = new Message.Sent(PacketIDs.MAP_LINE_DATA, clientID, ++lastSentMessageID);
try {
// but we need the line index!
line.getWriter().writeInt(i);
line.getWriter().writeUTF(lines.get(i));
} catch (IOException e){
e.printStackTrace();
}
sendMessage(line);
}
}).start();
}
public void sendAndLogLn(String str){
Message.Sent mess = new Message.Sent(PacketIDs.MESSAGE, clientID, ++lastSentMessageID);
try {
mess.getWriter().writeUTF(str + "\n");
sendMessage(mess);
System.out.println(str);
} catch (IOException e){
e.printStackTrace();
}
}
public void sendMessage(Message.Sent message){ public void sendMessage(Message.Sent message){
if (message.getPacketID() != PacketTable.ACK) if (message.getPacketID() != PacketIDs.ACK)
this.sentMessages.put(message.getMessageID(), message); this.sentMessages.put(message.getMessageID(), message);
byte[] data = message.getData().toByteArray(); byte[] data = message.getData().toByteArray();
if (data.length > PACKET_SIZE) if (data.length > PACKET_SIZE)
@ -198,7 +339,7 @@ public class Server implements Runnable {
DatagramPacket request = new DatagramPacket(data, data.length, address, port); DatagramPacket request = new DatagramPacket(data, data.length, address, port);
try { try {
System.out.println("Sending message with ID " + message.getMessageID() + " to client: " + message.getClientID() + " of type " + message.getPacketID()); System.out.println("Sending message with ID " + message.getMessageID() + " to client: " + message.getClientID() + " of type " + message.getPacketID());
serverSocket.send(request); server.socket.send(request);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }