- org.apache.maven.plugins
- maven-shade-plugin
- ${maven-shade-plugin.version}
+ org.openjfx
+ javafx-maven-plugin
+ 0.0.8
- package
-
- shade
-
+
+ default-cli
-
-
- edu.sdccd.cisc191.template.Client
-
-
-
-
- *:*
-
- META-INF/*.MF
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
-
-
-
+ app
+ app
+ app
+ true
+ true
+ true
diff --git a/Client/src/.DS_Store b/Client/src/.DS_Store
new file mode 100644
index 000000000..0787fb7b1
Binary files /dev/null and b/Client/src/.DS_Store differ
diff --git a/Client/src/main/.DS_Store b/Client/src/main/.DS_Store
new file mode 100644
index 000000000..c177c6a79
Binary files /dev/null and b/Client/src/main/.DS_Store differ
diff --git a/Client/src/main/java/.DS_Store b/Client/src/main/java/.DS_Store
new file mode 100644
index 000000000..da3a4031e
Binary files /dev/null and b/Client/src/main/java/.DS_Store differ
diff --git a/Client/src/main/java/edu/sdccd/cisc191/template/BetInfoView.java b/Client/src/main/java/edu/sdccd/cisc191/template/BetInfoView.java
new file mode 100644
index 000000000..9ee4c164b
--- /dev/null
+++ b/Client/src/main/java/edu/sdccd/cisc191/template/BetInfoView.java
@@ -0,0 +1,112 @@
+package edu.sdccd.cisc191.template;
+
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.chart.*;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * BetInfoView displays detailed information about a bet, including the bet amount, win amount,
+ * and the win odds over time. It uses a JavaFX application to render the UI components such as
+ * labels, charts, and buttons to allow users to view bet details and navigate back to the main view.
+ *
+ * Usage:
+ *
+ * BetInfoView view = new BetInfoView();
+ * view.betInfoView(primaryStage, bet);
+ *
+ *
+ */
+public class BetInfoView extends Application {
+ /**
+ * The bet object containing details to be displayed in the view.
+ */
+ Bet bet;
+
+ /**
+ * Launches the BetInfoView for a given Bet object.
+ *
+ * @param primaryStage the primary stage for the JavaFX application.
+ * @param bet the Bet object containing bet details.
+ * @throws Exception if an error occurs during view initialization.
+ */
+ public void betInfoView(Stage primaryStage, Bet bet) throws Exception {
+ this.bet = bet;
+ start(primaryStage);
+ }
+
+ /**
+ * Starts the JavaFX application by constructing the scene graph to display bet information.
+ * It builds a layout with bet details, monetary values, and a line chart showing win odds over time.
+ *
+ * @param stage the primary stage for the JavaFX application.
+ * @throws Exception if an error occurs during UI construction.
+ */
+ @Override
+ public void start(Stage stage) throws Exception {
+ VBox root = new VBox(10);
+ root.setStyle("-fx-alignment: center");
+
+ Label betInfo = new Label(bet.toString());
+
+ HBox money = new HBox(5);
+ Label betAmt = new Label("$" + bet.getBetAmt());
+ Label winAmt = new Label("$" + bet.getWinAmt());
+ money.getChildren().addAll(betAmt, winAmt);
+ money.setStyle("-fx-alignment: center");
+
+ // Set up the axes for the line chart.
+ CategoryAxis xAxis = new CategoryAxis();
+ xAxis.setLabel("Time");
+ NumberAxis yAxis = new NumberAxis();
+ yAxis.setLabel("Odds");
+
+ // Create the LineChart.
+ LineChart lineChart = new LineChart<>(xAxis, yAxis);
+ lineChart.setTitle("Win Odds Over Time");
+
+ // Create a data series.
+ XYChart.Series series = new XYChart.Series<>();
+ series.setName("Win Odds");
+
+ // Assume bet.getWinOddsOvertime() returns a double[][] where each element is [odd, timestamp]
+ double[][] oddsData = bet.getWinOddsOvertime();
+ // Iterate over each pair in the data array.
+ for (int i = 0; i < oddsData.length; i++) {
+ double odd = oddsData[i][0];
+ // The timestamp is stored as seconds since epoch.
+ long timestamp = (long) oddsData[i][1];
+ // Format the timestamp (multiply by 1000 to convert to milliseconds).
+ String timeLabel = new SimpleDateFormat("HH:mm").format(new Date(timestamp * 1000));
+ series.getData().add(new XYChart.Data<>(timeLabel, odd));
+ }
+
+ // Add the series to the chart.
+ lineChart.getData().add(series);
+
+ Button backButton = new Button("Back");
+ backButton.setOnAction(e -> {
+ try {
+ new Client().start(stage);
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ });
+ backButton.getStyleClass().add("primary-button");
+
+ root.getChildren().addAll(betInfo, money, lineChart, backButton);
+
+ Scene scene = new Scene(root, 800, 600);
+ scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
+
+ stage.setTitle("Bet Info");
+ stage.setScene(scene);
+ stage.show();
+ }
+}
diff --git a/Client/src/main/java/edu/sdccd/cisc191/template/BetView.java b/Client/src/main/java/edu/sdccd/cisc191/template/BetView.java
new file mode 100644
index 000000000..bbb414438
--- /dev/null
+++ b/Client/src/main/java/edu/sdccd/cisc191/template/BetView.java
@@ -0,0 +1,83 @@
+package edu.sdccd.cisc191.template;
+
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+
+/**
+ * The BetView class is a view class that creates
+ * the JavaFX window seen by the User after pressing the Bet button.
+ * It processes the bet amount entered by the user
+ */
+public class BetView extends Application {
+ /**
+ * The current game for which the bet is being placed.
+ */
+ Game game;
+
+ /**
+ * The team on which the bet is being placed.
+ */
+ String team;
+
+ /**
+ * Initializes the bet view with the specified stage, game, and team.
+ * It sets up the necessary data and starts the JavaFX application.
+ *
+ * @param stage the primary stage for this application.
+ * @param game the game object associated with the bet.
+ * @param team the team on which the bet is being placed.
+ * @throws Exception if an error occurs during initialization.
+ */
+ public void betView(Stage stage, Game game, String team) throws Exception {
+ this.game = game;
+ this.team = team;
+ start(stage);
+ }
+
+ /**
+ * Starts the JavaFX application by setting up the user interface for placing a bet.
+ * The interface includes a label, a text field for entering the bet amount, and a button to place the bet.
+ * When the bet is placed, the amount is validated against the user's available funds.
+ *
+ * @param stage the primary stage for this application.
+ * @throws Exception if an error occurs while setting up the scene.
+ */
+ @Override
+ public void start(Stage stage) throws Exception {
+ VBox betView = new VBox(10);
+ Label bet = new Label("How much do you want to bet?");
+ TextField b = new TextField();
+ Button b1 = new Button("Place Bet");
+
+ betView.getChildren().addAll(bet, b, b1);
+
+ b1.setOnAction(evt -> {
+ Integer amount = Integer.parseInt(b.getText());
+ if (Client.user.getMoneyBet() >= amount) {
+ Bet placedBet = new Bet(game, amount, team);
+ Client.user.addBet(placedBet);
+ try {
+ new Client().start(stage);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ } else {
+ //Creating a dialog
+ Dialog dialog = new Dialog<>();
+ //Setting the title
+ dialog.setTitle("Marauder Bets");
+ ButtonType type = new ButtonType("Ok", ButtonBar.ButtonData.OK_DONE);
+ //Setting the content of the dialog
+ dialog.setContentText("This is more money than you have available to bet! $" + Client.user.getMoneyBet());
+ //Adding buttons to the dialog pane
+ dialog.getDialogPane().getButtonTypes().add(type);
+ dialog.showAndWait();
+ }
+ });
+ stage.setScene(new Scene(betView, 200, 300));
+ stage.show();
+ }
+}
diff --git a/Client/src/main/java/edu/sdccd/cisc191/template/BotBase.java b/Client/src/main/java/edu/sdccd/cisc191/template/BotBase.java
new file mode 100644
index 000000000..1530b6d09
--- /dev/null
+++ b/Client/src/main/java/edu/sdccd/cisc191/template/BotBase.java
@@ -0,0 +1,180 @@
+package edu.sdccd.cisc191.template;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * BotBase simulates automated betting bots that place bets every 2–3 minutes.
+ */
+public class BotBase {
+ private Timer timer;
+ private Random random;
+ ArrayList basketballGames = new ArrayList<>();
+
+ // TODO: create a networking utility object that contains all of the methods for talking to the server, so we don't have to copy them between Client and Bot classes
+ /**
+ * The socket used to connect to the server.
+ */
+ private Socket clientSocket;
+
+ private ObjectOutputStream out; // The output stream for sending requests to the server.
+ private ObjectInputStream in; // The input stream for receiving responses from the server.
+
+ public BotBase() throws Exception {
+ // 1) open socket *before* you ever send a request
+ startConnection("localhost", 4444);
+
+ // 2) set up your timer & randomness
+ timer = new Timer();
+ random = new Random();
+
+ // 3) now you can getBasketballGames() safely inside startBot()
+ startBot();
+
+ }
+ // --- Socket and Request Methods ---
+ /**
+ * Establishes a connection to the server using the provided IP address and port.
+ *
+ * @param ip the IP address of the server.
+ * @param port the port number on the server.
+ * @throws IOException if an I/O error occurs when opening the connection.
+ */
+ public void startConnection(String ip, int port) throws IOException {
+ clientSocket = new Socket(ip, port);
+
+ out = new ObjectOutputStream(clientSocket.getOutputStream());
+ in = new ObjectInputStream(clientSocket.getInputStream());
+ }
+
+
+ // Update stopConnection to check for null before closing resources:
+ public void stopConnection() throws IOException {
+ if (in != null) {
+ in.close();
+ }
+ if (out != null) {
+ out.close();
+ }
+ if (clientSocket != null) {
+ clientSocket.close();
+ }
+ }
+
+ /**
+ * Sends a request to the server and returns a response of the expected type.
+ *
+ * Never call this method directly, call one of its wrappers for safe usage.
+ *
+ * @param the type parameter corresponding to the expected response type.
+ * @return the response from the server cast to the specified type.
+ * @throws Exception if an error occurs during the request.
+ */
+ private T sendRequest(Request request, Class responseType) throws Exception {
+ // Write request
+ out.writeObject(request);
+ out.flush();
+
+ // read back whatever the server sent
+ Object raw = in.readObject();
+ System.out.println("Raw: " + raw);
+ System.out.println("Raw type: " + raw.getClass());
+ System.out.println("Response Type: " + responseType);
+
+ // cast into the expected type
+
+ try {
+ return responseType.cast(raw);
+ }
+ catch (ClassCastException e) {
+ System.out.println("ClassCastException, could not cast " + raw.getClass() + " to " + responseType);
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Retrieves a game object from the server by the specified ID.
+ *
+ * @param id the identifier of the game to retrieve.
+ * @return the Game object if found; null otherwise.
+ * @throws IOException if an I/O error occurs during the request.
+ */
+ public Game getRequest(int id, String type) throws IOException {
+ try {
+ this.startConnection("localhost", 4444);
+
+ // build a request object
+ Request req = new Request("Game", id);
+
+ return this.sendRequest(req, Game.class);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ this.stopConnection();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ private ArrayList getBasketballGames() throws Exception {
+ ArrayList basketballGames;
+ basketballGames = sendRequest(new Request("Basketball", 1), ArrayList.class);
+ return basketballGames;
+ }
+
+
+ // Starts the bot betting loop
+ private void startBot() throws Exception {
+ basketballGames = this.getBasketballGames();
+ scheduleNextBet();
+ }
+
+ // Schedules the next bet to happen in 2–3 minutes
+ private void scheduleNextBet() {
+ int delay = (2 + random.nextInt(2)) * 60 * 1000; // 2–3 minutes in milliseconds
+ System.out.println("Bet in " + delay);
+ int index = (int)(Math.random() * basketballGames.size());
+ Game gameToBet = basketballGames.get(index);
+
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ placeRandomBet(gameToBet);
+ scheduleNextBet(); // Schedule again after this bet
+ }
+ }, delay);
+ }
+
+ // Simulates placing a random bet
+ private void placeRandomBet(Game game) {
+ double betAmt = 10 + random.nextInt(91); // $10 to $100
+
+ int teamSelect = (Math.random() <= 0.5) ? 1 : 2;
+ String team;
+
+ if (teamSelect == 1) team = game.getTeam1(); else team = game.getTeam2();
+
+ // Create a new Bet object using the updated constructor, casting the double to int
+ Bet newBet = new Bet(game, (int) betAmt, team);
+ System.out.println("Bot placed bet: " + newBet);
+ }
+
+ public static void main(String[] args) {
+ try {
+ BotBase bot = new BotBase();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Client/src/main/java/edu/sdccd/cisc191/template/Client.java b/Client/src/main/java/edu/sdccd/cisc191/template/Client.java
index 240f9f6fe..87ad7ef70 100644
--- a/Client/src/main/java/edu/sdccd/cisc191/template/Client.java
+++ b/Client/src/main/java/edu/sdccd/cisc191/template/Client.java
@@ -1,51 +1,468 @@
package edu.sdccd.cisc191.template;
-import java.net.*;
+import javafx.application.Application;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontPosture;
+import javafx.stage.Stage;
+
import java.io.*;
+import java.net.Socket;
+import java.util.*;
/**
- * This program opens a connection to a computer specified
- * as the first command-line argument. If no command-line
- * argument is given, it prompts the user for a computer
- * to connect to. The connection is made to
- * the port specified by LISTENING_PORT. The program reads one
- * line of text from the connection and then closes the
- * connection. It displays the text that it read on
- * standard output. This program is meant to be used with
- * the server program, DateServer, which sends the current
- * date and time on the computer where the server is running.
+ * The Client class establishes a connection to a server, sends requests, and builds the JavaFX UI for the application.
+ * It handles game, user, and size requests, as well as modifications to user data.
*/
+public class Client extends Application {
+ /**
+ * A static user representing the client user. Initialized with name "Chase" and money 1000000.
+ */
+ public static User user = new User("Chase", 1000000);
-public class Client {
+ /**
+ * The socket used to connect to the server.
+ */
private Socket clientSocket;
- private PrintWriter out;
- private BufferedReader in;
+ private ObjectOutputStream out; // The output stream for sending requests to the server.
+ private ObjectInputStream in; // The input stream for receiving responses from the server.
+
+ // --- Socket and Request Methods ---
+ /**
+ * Establishes a connection to the server using the provided IP address and port.
+ *
+ * @param ip the IP address of the server.
+ * @param port the port number on the server.
+ * @throws IOException if an I/O error occurs when opening the connection.
+ */
public void startConnection(String ip, int port) throws IOException {
clientSocket = new Socket(ip, port);
- out = new PrintWriter(clientSocket.getOutputStream(), true);
- in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+
+ out = new ObjectOutputStream(clientSocket.getOutputStream());
+ in = new ObjectInputStream(clientSocket.getInputStream());
}
- public CustomerResponse sendRequest() throws Exception {
- out.println(CustomerRequest.toJSON(new CustomerRequest(1)));
- return CustomerResponse.fromJSON(in.readLine());
+ /**
+ * Sends a request to the server and returns a response of the expected type.
+ *
+ * Never call this method directly, call one of its wrappers for safe usage.
+ *
+ * @param the type parameter corresponding to the expected response type.
+ * @return the response from the server cast to the specified type.
+ * @throws Exception if an error occurs during the request.
+ */
+ T sendRequest(Request request, Class responseType) throws Exception {
+ // Write request
+ out.writeObject(request);
+ out.flush();
+
+ // read back whatever the server sent
+ Object raw = in.readObject();
+ System.out.println("Raw: " + raw);
+ System.out.println("Raw type: " + raw.getClass());
+ System.out.println("Response Type: " + responseType);
+
+ // cast into the expected type
+
+ try {
+ return responseType.cast(raw);
+ }
+ catch (ClassCastException e) {
+ System.out.println("ClassCastException, could not cast " + raw.getClass() + " to " + responseType);
+ }
+
+ return null;
+
}
+ // Update stopConnection to check for null before closing resources:
public void stopConnection() throws IOException {
- in.close();
- out.close();
- clientSocket.close();
+ if (in != null) {
+ in.close();
+ }
+ if (out != null) {
+ out.close();
+ }
+ if (clientSocket != null) {
+ clientSocket.close();
+ }
}
- public static void main(String[] args) {
+ /**
+ * Retrieves a game object from the server by the specified ID.
+ *
+ * @param id the identifier of the game to retrieve.
+ * @return the Game object if found; null otherwise.
+ * @throws IOException if an I/O error occurs during the request.
+ */
+ public Game getRequest(int id, String type) throws IOException {
Client client = new Client();
+
try {
- client.startConnection("127.0.0.1", 4444);
- System.out.println(client.sendRequest().toString());
- client.stopConnection();
- } catch(Exception e) {
+ client.startConnection("localhost", 4445);
+
+ // build a request object
+ Request req = new Request("Game", id);
+
+ return client.sendRequest(req, Game.class);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ client.stopConnection();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ /**
+ * Modifies a user on the server with the provided attributes and returns the updated user.
+ *
+ * @param id the identifier of the user to modify.
+ * @param modifiedAttributes a map containing the fields and their new values.
+ * Valid Fields: ("Name", "Money", "addBet", "removeBet")
+ * @return the updated User object if modification is successful; null otherwise.
+ * @throws IOException if an I/O error occurs during the request.
+ */
+ public User userModifyRequest(int id, Map modifiedAttributes) throws IOException {
+ Client client = new Client();
+ try {
+ client.startConnection("localhost", 4444);
+ System.out.println("Sending userModifyRequest with ID: " + id);
+ return client.sendRequest(new Request("ModifyUser", id, modifiedAttributes), User.class);
+ } catch (Exception e) {
e.printStackTrace();
}
+ stopConnection();
+ return null;
+ }
+
+ // --- UI Component Builders ---
+ // Build the table view for the games
+ /**
+ * Creates a TableView for displaying game information.
+ * The table includes columns for teams, game date, odds, and betting buttons.
+ *
+ * @param games the array of Game objects to display.
+ * @param stage the Stage on which the TableView is displayed.
+ * @return a TableView populated with game data.
+ */
+ private TableView createGameTableView(T[] games, Stage stage) {
+ TableView tableView = new TableView<>();
+
+ // Column for Team 1
+ TableColumn team1Col = new TableColumn<>("Team 1");
+ team1Col.setCellValueFactory(new PropertyValueFactory<>("team1"));
+ tableView.getColumns().add(team1Col);
+ team1Col.setPrefWidth(150);
+ team1Col.setResizable(false);
+ team1Col.setSortable(false);
+ team1Col.setReorderable(false);
+
+ // Column for Team 2
+ TableColumn team2Col = new TableColumn<>("Team 2");
+ team2Col.setCellValueFactory(new PropertyValueFactory<>("team2"));
+ tableView.getColumns().add(team2Col);
+ team2Col.setResizable(false);
+ team2Col.setPrefWidth(150);
+ team2Col.setSortable(false);
+ team2Col.setReorderable(false);
+
+ // Column for Date
+ TableColumn dateCol = new TableColumn<>("Date");
+ dateCol.setCellValueFactory(new PropertyValueFactory<>("dateClean"));
+ dateCol.setPrefWidth(175);
+ tableView.getColumns().add(dateCol);
+ dateCol.setResizable(false);
+ dateCol.setSortable(false);
+ dateCol.setReorderable(false);
+
+ // Column for Team 1 Odds
+ TableColumn team1OddCol = new TableColumn<>("Team 1 Odds");
+ team1OddCol.setCellValueFactory(new PropertyValueFactory<>("team1Odd"));
+ tableView.getColumns().add(team1OddCol);
+ team1OddCol.setResizable(false);
+ team1OddCol.setSortable(false);
+ team1OddCol.setReorderable(false);
+
+ // Button column for betting on Team 1
+ TableColumn bet1Column = new TableColumn<>("Bet");
+ bet1Column.setCellFactory(column -> new TableCell() {
+ private final Button betButton = new Button("Bet");
+ {
+ betButton.setOnAction(event -> {
+ int index = getIndex();
+ if (index >= 0 && index < getTableView().getItems().size()) {
+ T game = getTableView().getItems().get(index);
+ try {
+ new BetView().betView(stage, game, game.getTeam1());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ betButton.getStyleClass().add("primary-button");
+ });
+ }
+ @Override
+ protected void updateItem(Void item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty) {
+ setGraphic(null);
+ } else {
+ int index = getIndex();
+ if (index >= 0 && index < getTableView().getItems().size()) {
+ T game = getTableView().getItems().get(index);
+ betButton.setDisable(user.checkBet(game));
+ }
+ setGraphic(betButton);
+ }
+ }
+ });
+ tableView.getColumns().add(bet1Column);
+ bet1Column.setResizable(false);
+ bet1Column.setSortable(false);
+ bet1Column.setReorderable(false);
+
+ // Column for Team 2 Odds
+ TableColumn team2OddCol = new TableColumn<>("Team 2 Odds");
+ team2OddCol.setCellValueFactory(new PropertyValueFactory<>("team2Odd"));
+ tableView.getColumns().add(team2OddCol);
+ team2OddCol.setResizable(false);
+ team2OddCol.setSortable(false);
+ team2OddCol.setReorderable(false);
+
+ // Button column for betting on Team 2
+ TableColumn bet2Column = new TableColumn<>("Bet");
+ bet2Column.setCellFactory(column -> new TableCell() {
+ private final Button betButton = new Button("Bet");
+ {
+ betButton.setOnAction(event -> {
+ int index = getIndex();
+ if (index >= 0 && index < getTableView().getItems().size()) {
+ T game = getTableView().getItems().get(index);
+ try {
+ new BetView().betView(stage, game, game.getTeam2());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ betButton.getStyleClass().add("primary-button");
+ }
+ @Override
+ protected void updateItem(Void item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty) {
+ setGraphic(null);
+ } else {
+ int index = getIndex();
+ if (index >= 0 && index < getTableView().getItems().size()) {
+ T game = getTableView().getItems().get(index);
+ betButton.setDisable(user.checkBet(game));
+ }
+ setGraphic(betButton);
+ }
+ }
+ });
+ tableView.getColumns().add(bet2Column);
+ bet2Column.setResizable(false);
+ bet2Column.setSortable(false);
+ bet2Column.setReorderable(false);
+
+ tableView.getStyleClass().add("custom-table");
+
+ // Add game items to the table view
+ tableView.getItems().addAll(games);
+
+ return tableView;
+ }
+
+ /**
+ * Creates an HBox containing user information labels.
+ *
+ * @return an HBox with the user's name, money, money line, and money bet.
+ */
+ private HBox createUserInfoBox() {
+ HBox userInfo = new HBox(10);
+ userInfo.setBackground(Background.fill(Color.rgb(126, 24, 145)));
+
+ Label userName = new Label(user.getName());
+ userName.setId("userNameLabel");
+ userName.setFont(new Font(20));
+ userName.setTextFill(Color.WHITE);
+
+ Label money = new Label("$" + user.getMoney());
+ money.setFont(new Font(20));
+ money.setTextFill(Color.WHITE);
+
+ Label moneyLine = new Label("$" + user.getMoneyLine());
+ moneyLine.setFont(new Font(20));
+ moneyLine.setTextFill(Color.LIGHTGRAY);
+
+ Label moneyBet = new Label("$" + user.getMoneyBet());
+ moneyBet.setFont(new Font(20));
+ moneyBet.setTextFill(Color.LIGHTGRAY);
+
+ userInfo.getChildren().addAll(userName, money, moneyLine, moneyBet);
+ return userInfo;
+ }
+
+ /**
+ * Creates an HBox that displays the list of bets placed by the user.
+ * If no bets are present, a placeholder label is shown.
+ *
+ * @param stage the Stage on which the bet list is displayed.
+ * @return an HBox containing bet information.
+ */
+ private HBox createBetListBox(Stage stage) {
+ HBox betList = new HBox(10);
+ betList.setPrefHeight(200);
+
+ if (user.getBets().isEmpty()) {
+ Label emptyLabel = new Label("Your bets will appear here");
+ emptyLabel.setId("betListPlaceholderLabel");
+ emptyLabel.setFont(Font.font("System", FontPosture.ITALIC, 12));
+ betList.getChildren().add(emptyLabel);
+ } else {
+ for (Bet bet : user.getBets()) {
+ VBox betBox = new VBox(10);
+ betBox.setPrefHeight(200);
+ betBox.setPrefWidth(200);
+ betBox.getStyleClass().add("bet-box");
+
+ Label gameLabel = new Label(bet.getGame().getTeam1() + " vs " + bet.getGame().getTeam2());
+ Label dateLabel = new Label(bet.getGame().getDateClean());
+ Label teamLabel = new Label(bet.getBetTeam());
+
+ // Apply a common style class to all bet-related labels
+ gameLabel.getStyleClass().add("bet-box-label");
+ dateLabel.getStyleClass().add("bet-box-label");
+ teamLabel.getStyleClass().add("bet-box-label");
+
+ Label betAmt = new Label("Bet $" + bet.getBetAmt());
+ betAmt.setFont(new Font(10));
+ betAmt.setTextFill(Color.WHITE);
+ Label winAmt = new Label("Win $" + bet.getWinAmt());
+ winAmt.setFont(new Font(10));
+ winAmt.setTextFill(Color.WHITE);
+
+ Button betInfo = new Button("See More");
+ betInfo.getStyleClass().add("bet-info-button");
+ betInfo.setOnAction(event -> {
+ try {
+ new BetInfoView().betInfoView(stage, bet);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ betBox.getChildren().addAll(gameLabel, dateLabel, teamLabel, betAmt, winAmt, betInfo);
+ betList.getChildren().add(betBox);
+ }
+ }
+ return betList;
+ }
+
+ /**
+ * Retrieves an array of Game objects from the server.
+ *
+ * @return an array of Game objects.
+ * @throws IOException if an I/O error occurs during retrieval.
+ */
+ private Game[] getGames() throws Exception {
+ int size = sendRequest(new Request("GetSize", 1), Integer.class);
+ Game[] games = new Game[size];
+ for (int i = 0; i < size; i++) {
+ games[i] = sendRequest(new Request("Game", 1), Game.class);
+ }
+ return games;
+ }
+
+ private ArrayList getBasketballGames() throws Exception {
+ ArrayList basketballGames;
+ basketballGames = sendRequest(new Request("Basketball", 1), ArrayList.class);
+ return basketballGames;
+ }
+
+ private ArrayList getBaseballGames() throws Exception {
+ ArrayList baseballGames;
+ baseballGames = sendRequest(new Request("Baseball", 1), ArrayList.class);
+ return baseballGames;
+ }
+
+
+ /**
+ * The main entry point for the client application.
+ * Launches the JavaFX application.
+ *
+ * @param args command-line arguments.
+ */
+ public static void main(String[] args) throws IOException {
+ launch();// Run this Application.
+ }
+
+ @Override
+ /**
+ * Starts the JavaFX application by building the main layout.
+ * It sets up UI components such as the game table, user info box, and bet list,
+ * and then displays the primary stage.
+ *
+ * @param stage the primary Stage for this application.
+ * @throws Exception if an error occurs during initialization.
+ */
+ public void start(Stage stage) throws Exception {
+// System.out.println(getSizeRequest(1));
+ startConnection("localhost", 4445);
+ System.out.println(getBasketballGames());
+ // Test modification of user
+ Map attributes = new HashMap<>();
+ attributes.put("Name", "John");
+ attributes.put("Money", 9999);
+
+// System.out.println(userModifyRequest(2, attributes));
+
+// Game[] response = getGames();
+// System.out.println(response);
+ // Fetch games
+ ArrayList basketballGames = getBasketballGames();
+ ArrayList baseballGames = getBaseballGames();
+
+ ArrayList allGames = new ArrayList<>();
+ allGames.addAll(basketballGames);
+ allGames.addAll(baseballGames);
+
+ // Build the main layout
+ BorderPane borderPane = new BorderPane();
+ borderPane.setPadding(new Insets(20));
+
+ // Create UI components
+ TableView gameTable = createGameTableView(allGames.toArray(new Game[0]), stage);
+ HBox userInfoBox = createUserInfoBox();
+ HBox betListBox = createBetListBox(stage);
+
+ // Assemble components into the BorderPane
+ borderPane.setCenter(gameTable);
+ borderPane.setTop(userInfoBox);
+ borderPane.setBottom(betListBox);
+
+ // Create and set the scene
+ Scene scene = new Scene(borderPane, 1000, 800);
+ scene.getStylesheets().add(getClass().getResource("/styles.css").toExternalForm());
+ stage.setScene(scene);
+ stage.setTitle("Marauder Bets");
+ stage.show();
}
} //end class Client
diff --git a/Client/src/main/resources/styles.css b/Client/src/main/resources/styles.css
new file mode 100644
index 000000000..96650875a
--- /dev/null
+++ b/Client/src/main/resources/styles.css
@@ -0,0 +1,98 @@
+/* General Table Styling */
+.custom-table {
+ -fx-background-color: #ffffff;
+ -fx-table-cell-border-color: transparent;
+ -fx-padding: 5px;
+}
+
+/* Table Column Headers */
+.custom-table .column-header-background {
+ -fx-background-color: #E73879;
+}
+
+.custom-table .column-header, .custom-table .filler {
+ -fx-background-color: transparent;
+ -fx-border-color: transparent;
+}
+
+.custom-table .column-header .label {
+ -fx-text-fill: #ffffff;
+ -fx-font-weight: bold;
+}
+
+/* Table Cells */
+.custom-table .table-cell {
+ -fx-padding: 8px;
+ -fx-font-size: 14px;
+ -fx-alignment: CENTER_LEFT;
+}
+
+.custom-table .table-row-cell:even {
+ -fx-background-color: #ffffff;
+}
+
+/* Hover effect for rows */
+.custom-table .table-row-cell:hover {
+ -fx-background-color: #FCC737;
+}
+
+/* Selected row */
+.custom-table .table-row-cell:selected {
+ -fx-background-color: #F26B0F;
+ -fx-text-fill: #ffffff;
+}
+
+.button {
+ -fx-background-color: #4a90e2;
+ -fx-text-fill: #ffffff;
+ -fx-font-size: 14px;
+ -fx-font-weight: bold;
+ -fx-padding: 8px 16px;
+ -fx-background-radius: 8px;
+ -fx-cursor: hand;
+ -fx-border-color: transparent;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.2), 4, 0, 0, 2);
+}
+
+/* Hover Effect */
+.button:hover {
+ -fx-background-color: #3c7dc2;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 6, 0, 0, 3);
+}
+
+/* Pressed Effect */
+.button:pressed {
+ -fx-background-color: #336ba6;
+ -fx-translate-y: 1px;
+ -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.25), 3, 0, 0, 1);
+}
+
+/* Disabled Button */
+.button:disabled {
+ -fx-background-color: #cfd7df;
+ -fx-text-fill: #9aa5b1;
+ -fx-cursor: default;
+ -fx-effect: none;
+}
+
+
+/* styles.css */
+.bet-box {
+ -fx-background-color: #F26B0F;
+ -fx-background-radius: 10;
+ -fx-padding: 10;
+ -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.3), 10, 0.5, 0, 0);
+}
+.bet-box-label {
+ -fx-font-size: 15;
+ -fx-text-fill: white;
+}
+.bet-info-button {
+ -fx-background-color: #ffffff;
+ -fx-text-fill: #F26B0F;
+ -fx-border-radius: 5;
+ -fx-cursor: hand;
+}
+.bet-info-button:hover {
+ -fx-background-color: #e0e0e0;
+}
diff --git a/Client/src/test/java/edu/sdccd/cisc191/template/BetInfoViewTest.java b/Client/src/test/java/edu/sdccd/cisc191/template/BetInfoViewTest.java
new file mode 100644
index 000000000..4c914fef9
--- /dev/null
+++ b/Client/src/test/java/edu/sdccd/cisc191/template/BetInfoViewTest.java
@@ -0,0 +1,95 @@
+package edu.sdccd.cisc191.template;
+
+import javafx.application.Platform;
+import javafx.collections.ObservableList;
+import javafx.scene.Scene;
+import javafx.scene.chart.LineChart;
+import javafx.scene.chart.XYChart;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.util.TimeZone;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class BetInfoViewTest {
+
+ @BeforeAll
+ public static void initJFX() throws Exception {
+ // Set the default timezone to UTC so that time labels are predictable.
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+ if (!Platform.isFxApplicationThread()) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ Platform.startup(() -> latch.countDown());
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw new Exception("JavaFX platform did not start.");
+ }
+ }
+ }
+
+ // Dummy implementation of Bet to supply known data.
+ private static class DummyBet extends Bet {
+ @Override
+ public double[][] getWinOddsOvertime() {
+ // Return two data points:
+ // First point: odd=1.5 at timestamp 1609459200 (which formats to "00:00" in UTC)
+ // Second point: odd=2.0 at timestamp 1609462800 (which formats to "01:00" in UTC)
+ return new double[][] {
+ {1.5, 1609459200},
+ {2.0, 1609462800}
+ };
+ }
+ @Override
+ public String toString() {
+ return "Dummy Bet";
+ }
+ }
+
+ @Test
+ public void testDisplayWinOddsData() throws Exception {
+ CountDownLatch latch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ try {
+ Stage stage = new Stage();
+ DummyBet dummyBet = new DummyBet();
+ BetInfoView view = new BetInfoView();
+ // Launch the view using the betInfoView() method.
+ view.betInfoView(stage, dummyBet);
+ Scene scene = stage.getScene();
+ // Get the root layout (VBox) from the scene.
+ VBox root = (VBox) scene.getRoot();
+ // The line chart is added as the third child (index 2) in the VBox.
+ LineChart, ?> lineChart = (LineChart, ?>) root.getChildren().get(2);
+ ObservableList extends XYChart.Series, ?>> seriesList = lineChart.getData();
+ assertEquals(1, seriesList.size(), "There should be one data series in the line chart.");
+
+ XYChart.Series series = (XYChart.Series) seriesList.get(0);
+ assertEquals("Win Odds", series.getName(), "Series name should be 'Win Odds'.");
+ ObservableList> dataPoints = series.getData();
+ assertEquals(2, dataPoints.size(), "Series should contain 2 data points.");
+
+ // Validate the first data point.
+ XYChart.Data firstData = dataPoints.get(0);
+ assertEquals("00:00", firstData.getXValue(), "First data point time label should be '00:00'.");
+ assertEquals(1.5, firstData.getYValue().doubleValue(), 0.001, "First data point odd should be 1.5.");
+
+ // Validate the second data point.
+ XYChart.Data secondData = dataPoints.get(1);
+ assertEquals("01:00", secondData.getXValue(), "Second data point time label should be '01:00'.");
+ assertEquals(2.0, secondData.getYValue().doubleValue(), 0.001, "Second data point odd should be 2.0.");
+ } catch(Exception e) {
+ e.printStackTrace();
+ fail("Exception in Platform.runLater: " + e.getMessage());
+ } finally {
+ latch.countDown();
+ }
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("Test did not complete in time.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Client/src/test/java/edu/sdccd/cisc191/template/ClientTest.java b/Client/src/test/java/edu/sdccd/cisc191/template/ClientTest.java
new file mode 100644
index 000000000..991e8ea53
--- /dev/null
+++ b/Client/src/test/java/edu/sdccd/cisc191/template/ClientTest.java
@@ -0,0 +1,118 @@
+package edu.sdccd.cisc191.template;
+
+import javafx.application.Platform;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableView;
+import javafx.stage.Stage;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class ClientTest {
+
+ private Client client;
+ private Stage primaryStage;
+ private Scene scene;
+
+ @BeforeAll
+ public static void initJFX() throws Exception {
+ if (!Platform.isFxApplicationThread()) {
+ final CountDownLatch latch = new CountDownLatch(1);
+ // Initialize the JavaFX Platform
+ Platform.startup(() -> {
+ // No need to do anything here
+ latch.countDown();
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw new Exception("JavaFX platform failed to start.");
+ }
+ }
+ }
+
+ private void setupStageAndScene() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ try {
+ primaryStage = new Stage();
+ client = new Client();
+ client.start(primaryStage);
+ primaryStage.show();
+ scene = primaryStage.getScene();
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ latch.countDown();
+ }
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ throw new Exception("Setup did not complete in time.");
+ }
+ }
+
+ @Test
+ public void testStageTitle() throws Exception {
+ setupStageAndScene();
+ final CountDownLatch latch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ assertEquals("Marauder Bets", primaryStage.getTitle(), "Stage title should be 'Marauder Bets'.");
+ latch.countDown();
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("testStageTitle did not complete in time.");
+ }
+ }
+
+ @Test
+ public void testTableViewExists() throws Exception {
+ setupStageAndScene();
+ final CountDownLatch latch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ Parent root = scene.getRoot();
+ TableView> tableView = (TableView>) root.lookup(".custom-table");
+ assertNotNull(tableView, "TableView with style class 'custom-table' should be present.");
+ latch.countDown();
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("testTableViewExists did not complete in time.");
+ }
+ }
+
+ @Test
+ public void testUserInfoLabel() throws Exception {
+ setupStageAndScene();
+ final CountDownLatch latch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ Parent root = scene.getRoot();
+ // Assumes the user name label has been assigned an fx:id of "userNameLabel"
+ Label userNameLabel = (Label) root.lookup("#userNameLabel");
+ assertNotNull(userNameLabel, "User info label displaying the user's name should be present.");
+ // Assuming Client.user is available and properly initialized
+ assertEquals(Client.user.getName(), userNameLabel.getText(), "User info label should display the user's name.");
+ latch.countDown();
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("testUserInfoLabel did not complete in time.");
+ }
+ }
+
+ @Test
+ public void testBetListPlaceholder() throws Exception {
+ setupStageAndScene();
+ final CountDownLatch latch = new CountDownLatch(1);
+ Platform.runLater(() -> {
+ Parent root = scene.getRoot();
+ // Assumes the bet list placeholder label has been assigned an fx:id of "betListPlaceholderLabel"
+ Label placeholderLabel = (Label) root.lookup("#betListPlaceholderLabel");
+ assertNotNull(placeholderLabel, "Placeholder label for bet list should be visible when no bets exist.");
+ latch.countDown();
+ });
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ fail("testBetListPlaceholder did not complete in time.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Client/src/test/java/edu/sdccd/cisc191/template/MultiClientTest.java b/Client/src/test/java/edu/sdccd/cisc191/template/MultiClientTest.java
new file mode 100644
index 000000000..68cf3d259
--- /dev/null
+++ b/Client/src/test/java/edu/sdccd/cisc191/template/MultiClientTest.java
@@ -0,0 +1,84 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests to see if we can safely run multiple clients.
+ * Simulates concurrent modifications to a single user object through
+ * multiple client connections and validates server-client communication.
+ * Need to run Server.java before running this test.
+ * Will fail if any threads are dropped.
+ */
+class MultiClientTest {
+
+ /**
+ * Simulates concurrent client requests to modify a single user object, with the server running.
+ */
+ @Test
+ void testConcurrentUserModifications() throws Exception {
+
+ // Initialize the client
+ Client client = new Client();
+ client.startConnection("localhost", 4444);
+
+ // Fetch initial state of the user
+ User initialUser = client.userGetRequest(2);
+ assertNotNull(initialUser, "Initial user state should not be null.");
+ System.out.println(String.format("Initial User State: Name[%s], Money[%d]", initialUser.getName(), initialUser.getMoney()));
+ client.stopConnection();
+
+ // Create a thread pool for concurrent client requests
+ ExecutorService executor = Executors.newFixedThreadPool(10); // Adjust thread pool size for testing
+ int testRuns = 50; // Reduce test runs for quicker execution in tests
+ for (int i = 0; i < testRuns; i++) {
+ executor.submit(() -> {
+ try {
+ client.startConnection("localhost", 4444);
+
+ // Modify User ID 2 with a unique modification
+ Map attributes = new HashMap<>();
+ attributes.put("Name", "User" + Thread.currentThread().getId());
+ attributes.put("Increment Money", 1);
+
+ User modifiedUser = client.userModifyRequest(2, attributes);
+ assertNotNull(modifiedUser, "Modified user state should not be null.");
+ System.out.println("Modified User: " + modifiedUser);
+
+ client.stopConnection();
+ // Stagger the requests
+ Thread.sleep((long) (Math.random() * 100));
+ } catch (Exception e) {
+ e.printStackTrace();
+ fail("Exception occurred during client modification: " + e.getMessage());
+ }
+ });
+ }
+
+ // Shut down the executor after tasks complete
+ executor.shutdown();
+ while (!executor.isTerminated()) {
+ // Wait for all tasks to finish
+ }
+
+ // Retrieve the final state of User ID 2
+ client.startConnection("localhost", 4444);
+ User finalUser = client.userGetRequest(2);
+ assertNotNull(finalUser, "Final user state should not be null.");
+ System.out.println(String.format("Final User State: Name[%s], Money[%d]", finalUser.getName(), finalUser.getMoney()));
+
+ // Assertions to validate modifications. Subtract 1 because we start from 0.
+ assertEquals(initialUser.getMoney() + testRuns, finalUser.getMoney(),
+ "Final user's money should increase by the total amount of modifications.");
+ assertTrue(finalUser.getName().startsWith("User"), "Final user's name should reflect the last modification.");
+ System.out.println("Assertions passed!");
+ client.stopConnection();
+
+ }
+}
diff --git a/Common/pom.xml b/Common/pom.xml
index 061089a4a..a3be890b7 100644
--- a/Common/pom.xml
+++ b/Common/pom.xml
@@ -16,6 +16,12 @@
jackson-databind
2.18.2
+
+ org.junit.jupiter
+ junit-jupiter
+ RELEASE
+ test
+
diff --git a/Common/src/main/java/edu/sdccd/cisc191/template/Bet.java b/Common/src/main/java/edu/sdccd/cisc191/template/Bet.java
new file mode 100644
index 000000000..8604b0aef
--- /dev/null
+++ b/Common/src/main/java/edu/sdccd/cisc191/template/Bet.java
@@ -0,0 +1,237 @@
+package edu.sdccd.cisc191.template;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.Serializable;
+import java.util.Random;
+
+/**
+ * Represents a bet placed on a game. Contains information about the game,
+ * the team being bet on, the bet amount, potential winnings, and the
+ * odds of winning. Additionally, it tracks odds over time.
+ *
+ * The class supports JSON serialization and deserialization for integration
+ * with external systems and persistent storage.
+ *
+ * @author Brian Tran, Andy Ly, Julian Garcia
+ * @see Game
+ * @see User
+ */
+public class Bet implements Serializable {
+
+ private Game game;
+ private String betTeam;
+ private int betAmt;
+ private int winAmt;
+ private int winOdds;
+
+ private final int numHours = 10; // Number of hours to track odds
+ private final double[][] winOddsOvertime = new double[numHours][2]; // Array to track odds over time
+
+ private boolean fulfillment;
+ private final long currentEpochSeconds = System.currentTimeMillis() / 1000; // Current time in seconds
+
+ @JsonIgnore
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Serializes a Bet object into a JSON string.
+ *
+ * @param bet The Bet object to serialize.
+ * @return A JSON string representation of the Bet .
+ * @throws Exception If serialization fails.
+ */
+ public static String toJSON(Bet bet) throws Exception {
+ return objectMapper.writeValueAsString(bet);
+ }
+
+ /**
+ * Deserializes a JSON string into a Bet object.
+ *
+ * @param input The JSON string to deserialize.
+ * @return A Bet object created from the JSON string.
+ * @throws Exception If deserialization fails.
+ */
+ public static Bet fromJSON(String input) throws Exception {
+ System.out.println(input);
+ return objectMapper.readValue(input, Bet.class);
+ }
+
+ /**
+ * Default constructor for Bet .
+ * Required for JSON serialization/deserialization.
+ */
+ protected Bet() {
+ // Default constructor for deserialization purposes
+ }
+
+ private final Random random = new Random();
+
+ /**
+ * Constructs a new Bet with specified game, bet amount, and team.
+ * Initializes potential winnings, odds of winning, and odds tracking over time.
+ *
+ * @param g The game associated with the bet.
+ * @param amt The amount of money being bet.
+ * @param betTeam The team being bet on.
+ */
+ public Bet(Game g, int amt, String betTeam) {
+ this.game = g;
+ this.betTeam = betTeam;
+ this.betAmt = amt;
+
+ if (betTeam.equalsIgnoreCase("team1")) {
+ winOdds = (int) Game.getTeam1Odd();
+ } else if (betTeam.equalsIgnoreCase("team2")) {
+ winOdds = (int) Game.getTeam2Odd();
+
+ if (winOdds >= 0) {
+ this.winAmt = (amt + (100 / winOdds) * amt);
+ } else {
+ this.winAmt = (amt + Math.abs((winOdds / 100) * amt));
+ }
+ }
+
+ // Populate winOddsOvertime with odds and timestamps
+ for (int j = 0; j < numHours; j++) {
+ long timeStamp = currentEpochSeconds - (j * 3600L); // Decrement by hours
+ double odd = calculateOddsForGameAtTime(timeStamp);
+ winOddsOvertime[j][0] = odd;
+ winOddsOvertime[j][1] = timeStamp;
+ }
+ }
+
+
+
+ /**
+ * Calculates the odds for a game at a specific timestamp.
+ *
+ * @param timeStamp The timestamp for which to calculate the odds.
+ * @return A random value representing the odds at the specified time.
+ */
+ private double calculateOddsForGameAtTime(long timeStamp) {
+ return 1 + random.nextInt(100); // Generate a random value between 1 and 100
+ }
+
+ /**
+ * Gets the potential winnings for the bet.
+ *
+ * @return The winning amount.
+ */
+ public int getWinAmt() {
+ return winAmt;
+ }
+
+ /**
+ * Sets the potential winnings for the bet.
+ *
+ * @param winAmt The winning amount to set.
+ */
+ public void setWinAmt(int winAmt) {
+ this.winAmt = winAmt;
+ }
+
+ /**
+ * Gets the game associated with the bet.
+ *
+ * @return The associated game.
+ */
+ public Game getGame() {
+ return game;
+ }
+
+ /**
+ * Sets the game associated with the bet.
+ *
+ * @param game The game to set.
+ */
+ public void setGame(Game game) {
+ this.game = game;
+ }
+
+ /**
+ * Gets the odds of winning the bet.
+ *
+ * @return The odds of winning as a percentage.
+ */
+ public double getWinOdds() {
+ return winOdds;
+ }
+
+ /**
+ * Gets the odds tracked over a 10-hour period.
+ *
+ * @return A 2D array representing odds and timestamps.
+ */
+ public double[][] getWinOddsOvertime() {
+ return winOddsOvertime;
+ }
+
+ /**
+ * Updates the user's money based on the outcome of the bet.
+ *
+ * @param user The user associated with the bet.
+ * @return The updated user object.
+ */
+ public User updateUser(User user) {
+ if (fulfillment) {
+ user.setMoney(user.getMoney() + winAmt);
+ } else {
+ user.setMoney(user.getMoney() - winAmt);
+ }
+ return user;
+ }
+
+ /**
+ * Updates the fulfillment status of the bet based on the odds of winning.
+ */
+ public void updateFulfillment() {
+ int randomNumber = random.nextInt(100) + 1; // Generate a number from 1 to 100
+ fulfillment = randomNumber <= winOdds;
+ }
+
+ /**
+ * Gets the fulfillment status of the bet based on the odds of winning.
+ */
+ public boolean getFulfillment() {
+ return this.fulfillment;
+ }
+
+ /**
+ * Gets the team being bet on.
+ *
+ * @return The team being bet on.
+ */
+ public String getBetTeam() {
+ return betTeam;
+ }
+
+ /**
+ * Gets the bet amount.
+ *
+ * @return The bet amount.
+ */
+ public int getBetAmt() {
+ return betAmt;
+ }
+
+ /**
+ * Sets the bet amount.
+ *
+ * @param betAmt The amount of money to set for the bet.
+ */
+ public void setBetAmt(int betAmt) {
+ this.betAmt = betAmt;
+ }
+
+ /**
+ * Converts the Bet object into a string representation.
+ *
+ * @return A string describing the bet.
+ */
+ @Override
+ public String toString() {
+ return "Bet on " + game + " for " + betAmt;
+ }
+}
diff --git a/Common/src/main/java/edu/sdccd/cisc191/template/CustomerRequest.java b/Common/src/main/java/edu/sdccd/cisc191/template/CustomerRequest.java
deleted file mode 100644
index 477f640f2..000000000
--- a/Common/src/main/java/edu/sdccd/cisc191/template/CustomerRequest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package edu.sdccd.cisc191.template;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-public class CustomerRequest {
- private Integer id;
-
- @JsonIgnore
- private static final ObjectMapper objectMapper = new ObjectMapper();
- public static String toJSON(CustomerRequest customer) throws Exception {
- return objectMapper.writeValueAsString(customer);
- }
- public static CustomerRequest fromJSON(String input) throws Exception{
- return objectMapper.readValue(input, CustomerRequest.class);
- }
- protected CustomerRequest() {}
-
- public CustomerRequest(Integer id) {
- this.id = id;
- }
-
- @Override
- public String toString() {
- return String.format(
- "Customer[id=%d]",
- id);
- }
-
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-}
\ No newline at end of file
diff --git a/Common/src/main/java/edu/sdccd/cisc191/template/CustomerResponse.java b/Common/src/main/java/edu/sdccd/cisc191/template/CustomerResponse.java
deleted file mode 100644
index 80f43321f..000000000
--- a/Common/src/main/java/edu/sdccd/cisc191/template/CustomerResponse.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package edu.sdccd.cisc191.template;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-public class CustomerResponse {
- private Integer id;
- private String firstName;
- private String lastName;
-
- @JsonIgnore
- private static final ObjectMapper objectMapper = new ObjectMapper();
- public static String toJSON(CustomerResponse customer) throws Exception {
- return objectMapper.writeValueAsString(customer);
- }
- public static CustomerResponse fromJSON(String input) throws Exception{
- return objectMapper.readValue(input, CustomerResponse.class);
- }
- protected CustomerResponse() {}
-
- public CustomerResponse(Integer id, String firstName, String lastName) {
- this.id = id;
- this.firstName = firstName;
- this.lastName = lastName;
- }
-
- @Override
- public String toString() {
- return String.format(
- "Customer[id=%d, firstName='%s', lastName='%s']",
- id, firstName, lastName);
- }
-
- public Integer getId() {
- return id;
- }
-
- public String getFirstName() {
- return firstName;
- }
-
- public String getLastName() {
- return lastName;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public void setFirstName(String firstName) {
- this.firstName = firstName;
- }
-
- public void setLastName(String lastName) {
- this.lastName = lastName;
- }
-}
\ No newline at end of file
diff --git a/Common/src/main/java/edu/sdccd/cisc191/template/Game.java b/Common/src/main/java/edu/sdccd/cisc191/template/Game.java
new file mode 100644
index 000000000..18a5c98eb
--- /dev/null
+++ b/Common/src/main/java/edu/sdccd/cisc191/template/Game.java
@@ -0,0 +1,215 @@
+package edu.sdccd.cisc191.template;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Represents a game between two teams with details such as start and end dates,
+ * betting odds, and a clean date string representation.
+ *
+ * Supports JSON serialization and deserialization for easy integration
+ * with external systems. Also includes methods for comparing game objects.
+ *
+ * @author Andy Ly, Julian Garcia
+ */
+public class Game implements Serializable {
+
+ private String team1;
+ private String team2;
+ private Date date;
+ private String dateClean;
+ private static double team1Odd;
+ private static double team2Odd;
+ public static boolean getSelectedTeam;
+ public static boolean getTeam1;
+ public static boolean getTeam2;
+ public double team1Wager;
+ public double team2Wager;
+ public double betPool;
+ public double team1PayoutRatio;
+ public double team2PayoutRatio;
+ public double team1ProfitFactor;
+ public double team2ProfitFactor;
+
+ @JsonIgnore
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Serializes a Game object into a JSON string.
+ *
+ * @param customer The Game object to serialize.
+ * @return A JSON string representation of the Game .
+ * @throws Exception If serialization fails.
+ */
+ public static String toJSON(Game customer) throws Exception {
+ return objectMapper.writeValueAsString(customer);
+ }
+
+ /**
+ * Deserializes a JSON string into a Game object.
+ *
+ * @param input The JSON string to deserialize.
+ * @return A Game object created from the JSON string.
+ * @throws Exception If deserialization fails.
+ */
+ public static Game fromJSON(String input) throws Exception {
+ return objectMapper.readValue(input, Game.class);
+ }
+
+ /**
+ * Default constructor for Game .
+ * Required for JSON serialization/deserialization.
+ */
+ protected Game() {
+ // Default constructor for deserialization purposes
+ }
+
+ /**
+ * Creates a Game object with betting odds loaded from an API.
+ *
+ * Does not calculate any odds.
+ *
+ * @param t1 The name of team 1.
+ * @param t2 The name of team 2.
+ * @param date The date of the game.
+ * @param team1Odd The odds for team 1.
+ * @param team2Odd The odds for team 2.
+ */
+ public Game(String t1, String t2, Date date, double team1Odd, double team2Odd) {
+ this.team1 = t1;
+ this.team2 = t2;
+ this.date = date;
+
+ this.team1Odd = team1Odd;
+ this.team2Odd = team2Odd;
+ this.dateClean = this.getDateClean();
+ }
+
+
+ /**
+ * Generates a string representation of the game.
+ *
+ * @return A string describing the game details.
+ */
+ @Override
+ public String toString() {
+ return team1 + " vs. " + team2 + " on " + date.getMonth() + "/" + date.getDate() + "/" + (date.getYear() + 1900);
+ }
+
+ /**
+ * Compares two Game objects for equality based on teams,
+ * dates, and odds.
+ *
+ * @param obj The other object to compare with.
+ * @return true if the two objects are equal, otherwise false .
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ Game game = (Game) obj;
+
+ boolean team1Equals = Objects.equals(this.team1, game.getTeam1());
+ boolean team2Equals = Objects.equals(this.team2, game.getTeam2());
+ boolean startDateEquals = this.date.compareTo(game.getDate()) == 0;
+ boolean team1OddEquals = Math.abs(this.team1Odd - game.getTeam1Odd()) < 0.0001;
+ boolean team2OddEquals = Math.abs(this.team2Odd - game.getTeam2Odd()) < 0.0001;
+
+ return team1Equals && team2Equals && startDateEquals && team1OddEquals && team2OddEquals;
+ }
+
+ /**
+ * Gets the name of team 1.
+ *
+ * @return The name of team 1.
+ */
+ public String getTeam1() {
+ return team1;
+ }
+
+ /**
+ * Gets the name of team 2.
+ *
+ * @return The name of team 2.
+ */
+ public String getTeam2() {
+ return team2;
+ }
+
+ /**
+ * Gets the betting odds for team 1.
+ *
+ * @return The betting odds for team 1.
+ */
+ public static double getTeam1Odd() {
+ return team1Odd;
+ }
+
+ /**
+ * Gets the betting odds for team 2.
+ *
+ * @return The betting odds for team 2.
+ */
+ public static double getTeam2Odd() {
+ return team2Odd;
+ }
+
+ /**
+ * Gets the start date of the game.
+ *
+ * @return The start date.
+ */
+ public Date getDate() {
+ return this.date;
+ }
+
+
+ /**
+ * Generates a clean string representation of the date range for the game.
+ *
+ * Offsets are because of the way java.util.Date works.
+ *
+ * @return A string describing the start and end dates.
+ */
+ public String getDateClean() {
+ return (date.getMonth() + 1) + "/" + date.getDate() + "/" + (date.getYear() + 1900);
+
+ }
+
+ /**
+ * Sets the name of team 1.
+ *
+ * @param team1 The new name for team 1.
+ */
+ public void setTeam1(String team1) {
+ this.team1 = team1;
+ }
+
+ /**
+ * Sets the name of team 2.
+ *
+ * @param team2 The new name for team 2.
+ */
+ public void setTeam2(String team2) {
+ this.team2 = team2;
+ }
+
+ /**
+ * Sets the start date of the game.
+ *
+ * @param startDate The new start date.
+ */
+ public void setStartDate(Date startDate) {
+ this.date = startDate;
+ }
+
+}
\ No newline at end of file
diff --git a/Common/src/main/java/edu/sdccd/cisc191/template/Request.java b/Common/src/main/java/edu/sdccd/cisc191/template/Request.java
new file mode 100644
index 000000000..55ddb1eeb
--- /dev/null
+++ b/Common/src/main/java/edu/sdccd/cisc191/template/Request.java
@@ -0,0 +1,133 @@
+package edu.sdccd.cisc191.template;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Represents a customer request within the system. Each request includes
+ * a request type, an associated ID, and optional attributes for modification.
+ *
+ * This class supports serialization to and deserialization from JSON format
+ * to enable easy data exchange.
+ *
+ * @author Andy Ly, Andrew Huang
+ */
+public class Request implements Serializable {
+
+ private Map attributesToModify;
+ private Integer id;
+ private String requestType;
+
+ @JsonIgnore
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Serializes a CustomerRequest object into a JSON string.
+ *
+ * @param customer The CustomerRequest object to serialize.
+ * @return A JSON string representation of the CustomerRequest .
+ * @throws Exception If serialization fails.
+ */
+ public static String toJSON(Request customer) throws Exception {
+ return objectMapper.writeValueAsString(customer);
+ }
+
+ /**
+ * Deserializes a JSON string into a CustomerRequest object.
+ *
+ * @param input The JSON string to deserialize.
+ * @return A CustomerRequest object created from the JSON string.
+ * @throws Exception If deserialization fails.
+ */
+ public static Request fromJSON(String input) throws Exception {
+ return objectMapper.readValue(input, Request.class);
+ }
+
+ /**
+ * Default constructor for CustomerRequest .
+ * Required for JSON serialization/deserialization.
+ */
+ protected Request() {
+ // Default constructor for deserialization purposes
+ }
+
+ /**
+ * Creates a CustomerRequest with a specified request type and ID.
+ *
+ * @param requestType The type of the request.
+ * @param id The ID associated with the request.
+ */
+ public Request(String requestType, Integer id) {
+ this.requestType = requestType;
+ this.id = id;
+ this.attributesToModify = new HashMap<>();
+ }
+
+ /**
+ * Creates a CustomerRequest with a specified request type, ID, and
+ * attributes to modify.
+ *
+ * @param requestType The type of the request.
+ * @param id The ID associated with the request.
+ * @param attributesToModify A map of attributes to be modified.
+ */
+ public Request(String requestType, int id, Map attributesToModify) {
+ this.requestType = requestType;
+ this.id = id;
+ this.attributesToModify = attributesToModify;
+ }
+
+ /**
+ * Retrieves the attributes to be modified for this request.
+ *
+ * @return A map containing the attributes to modify.
+ */
+ public Map getAttributesToModify() {
+ return attributesToModify;
+ }
+
+ /**
+ * Converts the CustomerRequest object into a string representation.
+ *
+ * @return A string containing the request type and ID.
+ */
+ @Override
+ public String toString() {
+ return String.format(
+ """
+ Request Type: [type=%s]
+ RequestID: [id=%d]
+ """, requestType, id);
+ }
+
+ /**
+ * Retrieves the ID associated with this request.
+ *
+ * @return The request ID.
+ */
+ public Integer getId() {
+ return id;
+ }
+
+ /**
+ * Retrieves the type of this request.
+ *
+ * @return The request type.
+ */
+ public String getRequestType() {
+ return requestType;
+ }
+
+ /**
+ * Sets the ID associated with this request.
+ *
+ * @param id The new request ID.
+ */
+ public void setId(Integer id) {
+ this.id = id;
+ }
+}
diff --git a/Common/src/main/java/edu/sdccd/cisc191/template/User.java b/Common/src/main/java/edu/sdccd/cisc191/template/User.java
new file mode 100644
index 000000000..a4fd7c327
--- /dev/null
+++ b/Common/src/main/java/edu/sdccd/cisc191/template/User.java
@@ -0,0 +1,208 @@
+package edu.sdccd.cisc191.template;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Represents a user in the system, holding information about their
+ * name, available money, money currently in bets, and a list of active bets.
+ * Provides methods to manage bets and serialize/deserialize the user object.
+ *
+ * The class is designed to integrate seamlessly with JSON-based systems,
+ * enabling data exchange and persistent storage.
+ *
+ * @author Andy Ly, Julian Garcia
+ */
+public class User implements Serializable {
+
+ private String name;
+ private int money;
+ private int moneyLine; // Money placed in active bets but not yet resolved
+ private int moneyBet; // Money available for future bets
+ private ArrayList bets = new ArrayList<>();
+
+ @JsonIgnore
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Serializes a User object into a JSON string.
+ *
+ * @param customer The User object to serialize.
+ * @return A JSON string representation of the User .
+ * @throws Exception If serialization fails.
+ */
+ public static String toJSON(User customer) throws Exception {
+ return objectMapper.writeValueAsString(customer);
+ }
+
+ /**
+ * Deserializes a JSON string into a User object.
+ *
+ * @param input The JSON string to deserialize.
+ * @return A User object created from the JSON string.
+ * @throws Exception If deserialization fails.
+ */
+ public static User fromJSON(String input) throws Exception {
+ return objectMapper.readValue(input, User.class);
+ }
+
+ /**
+ * Default constructor for User .
+ * Required for JSON serialization/deserialization.
+ */
+ protected User() {
+ // Default constructor for deserialization purposes
+ }
+
+ /**
+ * Creates a new User with the specified name and initial money.
+ * Initializes moneyLine to 0 and moneyBet equal to the initial money.
+ *
+ * @param name The name of the user.
+ * @param money The initial amount of money the user has.
+ */
+ public User(String name, int money) {
+ this.name = name;
+ this.money = money;
+ this.moneyLine = 0;
+ this.moneyBet = money;
+ }
+
+ /**
+ * Checks if the user has an active bet on the specified game.
+ *
+ * @param game The game to check for active bets.
+ * @return true if an active bet exists for the game, otherwise false.
+ */
+ public boolean checkBet(Game game) {
+ for (Bet bet : bets) {
+ boolean result = bet.getGame().equals(game);
+ System.out.println("Checking bet: " + bet.getGame() + " with game: " + game + " Result: " + result);
+ if (result) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Retrieves the amount of money currently in active bets.
+ *
+ * @return The amount of money in active bets.
+ */
+ public int getMoneyLine() {
+ return moneyLine;
+ }
+
+ /**
+ * Sets the amount of money currently in active bets.
+ *
+ * @param moneyLine The new amount of money in active bets.
+ */
+ public void setMoneyLine(int moneyLine) {
+ this.moneyLine = moneyLine;
+ }
+
+ /**
+ * Retrieves the amount of money available for future bets.
+ *
+ * @return The amount of money available for future bets.
+ */
+ public int getMoneyBet() {
+ return moneyBet;
+ }
+
+ /**
+ * Sets the amount of money available for future bets.
+ *
+ * @param moneyBet The new amount of money available for future bets.
+ */
+ public void setMoneyBet(int moneyBet) {
+ this.moneyBet = moneyBet;
+ }
+
+ /**
+ * Retrieves the user's name.
+ *
+ * @return The user's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user's name.
+ *
+ * @param name The new name of the user.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Retrieves the user's total money.
+ *
+ * @return The user's total money.
+ */
+ public int getMoney() {
+ return money;
+ }
+
+ /**
+ * Sets the user's total money.
+ *
+ * @param amt The amount of money to add to the user's balance.
+ */
+ public void setMoney(int amt) {
+ this.money = amt;
+ }
+
+ /**
+ * Increments the user's total money.
+ *
+ * @param amt The amount of money to add to the user's balance.
+ */
+ public void incrMoney(int amt) {
+ this.money += amt;
+ }
+
+ /**
+ * Decrements the user's total money.
+ *
+ * @param amt The amount of money to add to the user's balance.
+ */
+ public void decrMoney(int amt) {
+ this.money -= amt;
+ }
+ /**
+ * Retrieves the list of active bets for the user.
+ *
+ * @return A list of active bets.
+ */
+ public ArrayList getBets() {
+ return bets;
+ }
+
+ /**
+ * Adds a new bet to the user's list of active bets and updates the money balance accordingly.
+ *
+ * @param b The bet to add.
+ */
+ public void addBet(Bet b) {
+ bets.add(b);
+ moneyBet -= b.getBetAmt();
+ moneyLine += b.getBetAmt();
+ }
+
+ /**
+ * Removes a bet from the user's list of active bets.
+ *
+ * @param b The bet to remove.
+ */
+ public void removeBet(Bet b) {
+ bets.remove(b);
+ }
+}
diff --git a/Common/src/test/java/edu/sdccd/cisc191/template/BetTest.java b/Common/src/test/java/edu/sdccd/cisc191/template/BetTest.java
new file mode 100644
index 000000000..408eb51a8
--- /dev/null
+++ b/Common/src/test/java/edu/sdccd/cisc191/template/BetTest.java
@@ -0,0 +1,182 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the Bet class.
+ * This test class validates the functionality of the methods in the
+ * Bet class, including constructors, getters, setters, and other methods.
+ */
+public class BetTest {
+
+ private Bet bet;
+ private Game game;
+ private User user;
+
+ /**
+ * Sets up the test environment before each test.
+ */
+ @BeforeEach
+ public void setUp() {
+ Date startDate = new Date(125, 3, 10); // April 10, 2025 (month is 0-based)
+ Date endDate = new Date(125, 3, 15); // April 15, 2025 (year is 1900 based)
+ game = new Game("Team A", "Team B", startDate, endDate);
+ bet = new Bet(game, 100, "Team A");
+ user = new User("TestUser", 1000);
+ }
+
+ /**
+ * Tests the toJSON method for correct serialization.
+ */
+ @Test
+ public void testToJSON() throws Exception {
+ String json = Bet.toJSON(bet);
+ System.out.println("Serialized JSON: " + json);
+ assertNotNull(json, "JSON string should not be null.");
+ assertTrue(json.contains("\"betTeam\":\"Team A\""), "JSON string should contain the team name.");
+ }
+
+ /**
+ * Tests the fromJSON method for correct deserialization.
+ */
+ @Test
+ public void testFromJSON() throws Exception {
+ String json = Bet.toJSON(bet);
+ Bet deserializedBet = Bet.fromJSON(json);
+ assertNotNull(deserializedBet, "Deserialized Bet object should not be null.");
+ assertEquals("Team A", deserializedBet.getBetTeam(), "Deserialized Bet should have the correct team name.");
+ }
+
+ /**
+ * Tests the getWinAmt method for correct retrieval of win amount.
+ */
+ @Test
+ public void testGetWinAmt() {
+ assertEquals(0, bet.getWinAmt(), "Win amount should be correctly calculated.");
+ }
+
+ /**
+ * Tests the setWinAmt method for correct setting of win amount.
+ */
+ @Test
+ public void testSetWinAmt() {
+ bet.setWinAmt(200);
+ assertEquals(200, bet.getWinAmt(), "Win amount should be correctly set.");
+ }
+
+ /**
+ * Tests the getGame method for correct retrieval of the associated game.
+ */
+ @Test
+ public void testGetGame() {
+ assertEquals(game, bet.getGame(), "Game should be correctly retrieved.");
+ }
+
+ /**
+ * Tests the setGame method for correct setting of the associated game.
+ */
+ @Test
+ public void testSetGame() {
+ Date startDate = new Date(125, 3, 10); // April 10, 2025 (month is 0-based)
+ Date endDate = new Date(125, 3, 15); // April 15, 2025 (year is 1900 based)
+ Game newGame = new Game("Team C", "Team D", startDate, endDate);
+ bet.setGame(newGame);
+ assertEquals(newGame, bet.getGame(), "Game should be correctly set.");
+ }
+
+ /**
+ * Tests the getWinOddsOvertime method for correct retrieval of win odds over time.
+ */
+ @Test
+ public void testGetWinOddsOvertime() {
+ double[][] winOddsOvertime = bet.getWinOddsOvertime();
+ assertEquals(10, winOddsOvertime.length, "Win odds over time should track 10 hours.");
+ assertEquals(2, winOddsOvertime[0].length, "Each entry should contain odds and timestamp.");
+ }
+
+ /**
+ * Tests the updateUser method for correct updating of user money based on bet outcome.
+ */
+ @Test
+ public void testUpdateUser() {
+ bet.updateFulfillment();
+ int initialMoney = user.getMoney();
+ bet.updateUser(user);
+ if (bet.getFulfillment()) {
+ assertEquals(initialMoney + bet.getWinAmt(), user.getMoney(), "User money should increase by win amount if bet is fulfilled.");
+ } else {
+ assertEquals(initialMoney - bet.getWinAmt(), user.getMoney(), "User money should decrease by win amount if bet is not fulfilled.");
+ }
+ }
+
+ /**
+ * Tests the updateFulfillment method for correct updating of bet fulfillment status.
+ */
+ @Test
+ public void testUpdateFulfillment() {
+ bet.updateFulfillment();
+ assertTrue(bet.getFulfillment() || !bet.getFulfillment(), "Fulfillment status should be updated correctly.");
+ }
+
+ /**
+ * Tests the getBetTeam method for correct retrieval of the bet team.
+ */
+ @Test
+ public void testGetBetTeam() {
+ assertEquals("Team A", bet.getBetTeam(), "Bet team should be correctly retrieved.");
+ }
+
+ /**
+ * Tests the getBetAmt method for correct retrieval of the bet amount.
+ */
+ @Test
+ public void testGetBetAmt() {
+ assertEquals(100, bet.getBetAmt(), "Bet amount should be correctly retrieved.");
+ }
+
+ /**
+ * Tests the setBetAmt method for correct setting of the bet amount.
+ */
+ @Test
+ public void testSetBetAmt() {
+ bet.setBetAmt(200);
+ assertEquals(200, bet.getBetAmt(), "Bet amount should be correctly set.");
+ }
+
+ /**
+ * Tests the toString method for correct string representation of the Bet object.
+ */
+ @Test
+ public void testToString() {
+ String betString = bet.toString();
+ assertTrue(betString.contains("Bet on"), "String representation should contain 'Bet on'.");
+ assertTrue(betString.contains("Team A"), "String representation should contain the team name.");
+ }
+
+ /**
+ * Tests the 2D array capability of the winOddsOvertime field.
+ */
+ @Test
+ public void testWinOddsOvertimeArray() {
+ double[][] winOddsOvertime = bet.getWinOddsOvertime();
+ assertEquals(10, winOddsOvertime.length, "Win odds over time should track 10 hours.");
+ assertEquals(2, winOddsOvertime[0].length, "Each entry should contain odds and timestamp.");
+
+ // Validate the first entry
+ double firstOdd = winOddsOvertime[0][0];
+ long firstTimestamp = (long) winOddsOvertime[0][1];
+ assertTrue(firstOdd >= 1 && firstOdd <= 100, "First odd should be between 1 and 100.");
+ assertTrue(firstTimestamp <= System.currentTimeMillis() / 1000, "First timestamp should be a valid epoch time.");
+
+ // Validate the last entry
+ double lastOdd = winOddsOvertime[9][0];
+ long lastTimestamp = (long) winOddsOvertime[9][1];
+ assertTrue(lastOdd >= 1 && lastOdd <= 100, "Last odd should be between 1 and 100.");
+ assertTrue(lastTimestamp <= System.currentTimeMillis() / 1000, "Last timestamp should be a valid epoch time.");
+ }
+}
\ No newline at end of file
diff --git a/Common/src/test/java/edu/sdccd/cisc191/template/CustomerRequestTest.java b/Common/src/test/java/edu/sdccd/cisc191/template/CustomerRequestTest.java
index b6b6721cd..8d03c6b86 100644
--- a/Common/src/test/java/edu/sdccd/cisc191/template/CustomerRequestTest.java
+++ b/Common/src/test/java/edu/sdccd/cisc191/template/CustomerRequestTest.java
@@ -1,22 +1,128 @@
package edu.sdccd.cisc191.template;
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the CustomerRequest class.
+ * This test class ensures that the functionality of the methods in
+ * CustomerRequest performs as expected, including JSON
+ * serialization and deserialization, as well as attribute handling.
+ *
+ * @author Andy Ly
+ */
class CustomerRequestTest {
- private CustomerRequest customerRequest;
- @org.junit.jupiter.api.BeforeEach
+ private Request request;
+ private Map attributes;
+
+ /**
+ * Sets up test data before each test case.
+ */
+ @BeforeEach
void setUp() {
- customerRequest = new CustomerRequest(1);
+ attributes = new HashMap<>();
+ attributes.put("Name", "John");
+ attributes.put("Age", 30);
+
+ request = new Request("ModifyUser", 1, attributes);
}
- @org.junit.jupiter.api.Test
- void getCustomer() {
- assertEquals(customerRequest.toString(), "Customer[id=1]");
+ /**
+ * Tests the getter for getRequestType .
+ * Ensures the request type is correctly returned.
+ */
+ @Test
+ void testGetRequestType() {
+ assertEquals("ModifyUser", request.getRequestType(), "The request type should be 'ModifyUser'.");
}
- @org.junit.jupiter.api.Test
- void setCustomer() {
- assertEquals(customerRequest.toString(), "Customer[id=1]");
+ /**
+ * Tests the getter for getId .
+ * Ensures the request ID is correctly returned.
+ */
+ @Test
+ void testGetId() {
+ assertEquals(1, request.getId(), "The request ID should be 1.");
+ }
+
+ /**
+ * Tests the getter for getAttributesToModify .
+ * Ensures that the attributes are returned correctly.
+ */
+ @Test
+ void testGetAttributesToModify() {
+ assertEquals(attributes, request.getAttributesToModify(), "The attributes should match the expected values.");
+ }
+
+ /**
+ * Tests the setter for setId .
+ * Ensures the request ID is correctly set.
+ */
+ @Test
+ void testSetId() {
+ request.setId(2);
+ assertEquals(2, request.getId(), "The request ID should be updated to 2.");
+ }
+
+ /**
+ * Tests the toString method.
+ * Ensures that the string representation matches the expected format.
+ */
+ @Test
+ void testToString() {
+ String expected = """
+ Request Type: [type=ModifyUser]
+ RequestID: [id=1]
+ """;
+ assertEquals(expected, request.toString(), "The string representation of the request is incorrect.");
+ }
+
+ /**
+ * Tests JSON serialization using toJSON .
+ * Ensures that the serialized JSON matches the structure and values of the CustomerRequest object.
+ */
+ @Test
+ void testToJSON() throws Exception {
+ // Convert the request object to a JSON string
+ String json = Request.toJSON(request);
+
+ // Create the expected JSON string using the manually created object
+ String expectedJson = """
+ {"attributesToModify":{"Age":30, "Name":"John"},"id":1,"requestType":"ModifyUser"}
+ """;
+
+ // Assert that the serialized JSON matches the expected JSON string
+ assertEquals(expectedJson.replaceAll("\\s+", ""), json.replaceAll("\\s+", ""),
+ "The serialized JSON does not match the expected JSON.");
+ }
+
+
+ /**
+ * Tests JSON deserialization using fromJSON .
+ * Ensures that a JSON string is correctly deserialized into a CustomerRequest object.
+ */
+ @Test
+ void testFromJSON() throws Exception {
+ String json = """
+ {
+ "requestType": "ModifyUser",
+ "id": 1,
+ "attributesToModify": {
+ "Name": "John",
+ "Age": 30
+ }
+ }
+ """;
+ Request deserializedRequest = Request.fromJSON(json);
+ assertNotNull(deserializedRequest, "The deserialized CustomerRequest object should not be null.");
+ assertEquals("ModifyUser", deserializedRequest.getRequestType(), "The deserialized request type should be 'ModifyUser'.");
+ assertEquals(1, deserializedRequest.getId(), "The deserialized request ID should be 1.");
+ assertEquals(attributes, deserializedRequest.getAttributesToModify(), "The deserialized attributes should match the expected values.");
}
-}
\ No newline at end of file
+}
diff --git a/Common/src/test/java/edu/sdccd/cisc191/template/CustomerResponseTest.java b/Common/src/test/java/edu/sdccd/cisc191/template/CustomerResponseTest.java
deleted file mode 100644
index d17dca955..000000000
--- a/Common/src/test/java/edu/sdccd/cisc191/template/CustomerResponseTest.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package edu.sdccd.cisc191.template;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class CustomerResponseTest {
- private CustomerResponse customerResponse;
-
- @org.junit.jupiter.api.BeforeEach
- void setUp() {
- customerResponse = new CustomerResponse(1, "Test", "User");
- }
-
- @org.junit.jupiter.api.Test
- void getCustomer() {
- assertEquals(customerResponse.toString(), "Customer[id=1, firstName='Test', lastName='User']");
- }
-
- @org.junit.jupiter.api.Test
- void setCustomer() {
- customerResponse.setFirstName("User");
- customerResponse.setLastName("Test");
- assertEquals(customerResponse.toString(), "Customer[id=1, firstName='User', lastName='Test']");
- }
-}
\ No newline at end of file
diff --git a/Common/src/test/java/edu/sdccd/cisc191/template/GameTest.java b/Common/src/test/java/edu/sdccd/cisc191/template/GameTest.java
new file mode 100644
index 000000000..d361fb5fa
--- /dev/null
+++ b/Common/src/test/java/edu/sdccd/cisc191/template/GameTest.java
@@ -0,0 +1,126 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the Game class.
+ * This test class validates the functionality of the methods in the
+ * Game class, including constructors, getters, setters,
+ * and overridden methods.
+ *
+ * @author Andy Ly
+ */
+class GameTest {
+
+ private Game game1;
+ private Game game2;
+
+ /**
+ * Sets up test data before each test case.
+ */
+ @BeforeEach
+ void setUp() {
+ Date startDate = new Date(125, 3, 10); // April 10, 2025 (month is 0-based)
+ Date endDate = new Date(125, 3, 15); // April 15, 2025 (year is 1900 based)
+ game1 = new Game("Team A", "Team B", startDate, endDate);
+ game2 = new Game("Team C", "Team D", startDate, endDate, 50.0, 60.0);
+ }
+
+ /**
+ * Tests the constructor that initializes mock betting odds.
+ */
+ @Test
+ void testConstructorWithMockOdds() {
+ assertEquals("Team A", game1.getTeam1(), "Team 1 should be 'Team A'.");
+ assertEquals("Team B", game1.getTeam2(), "Team 2 should be 'Team B'.");
+ assertNotNull(game1.getStartDate(), "The start date should not be null.");
+ assertNotNull(game1.getEndDate(), "The end date should not be null.");
+ assertTrue(game1.getTeam1Odd() > 0, "Team 1 odds should be greater than 0.");
+ assertTrue(game1.getTeam2Odd() > 0, "Team 2 odds should be greater than 0.");
+ }
+
+ /**
+ * Tests the constructor that accepts predefined betting odds.
+ */
+ @Test
+ void testConstructorWithPredefinedOdds() {
+ assertEquals("Team C", game2.getTeam1(), "Team 1 should be 'Team C'.");
+ assertEquals("Team D", game2.getTeam2(), "Team 2 should be 'Team D'.");
+ assertEquals(50.0, game2.getTeam1Odd(), 0.0001, "Team 1 odds should be 50.0.");
+ assertEquals(60.0, game2.getTeam2Odd(), 0.0001, "Team 2 odds should be 60.0.");
+ }
+
+ /**
+ * Tests the getDateClean method to ensure it returns the correct date string.
+ */
+ @Test
+ void testGetDateClean() {
+ String expectedDateClean = "4/10/2025 - 4/15/2025";
+ assertEquals(expectedDateClean, game1.getDateClean(), "The cleaned date string is incorrect.");
+ }
+
+ /**
+ * Tests the toString method for correct output format.
+ */
+ @Test
+ void testToString() {
+ String expectedString = "Team A vs. Team B on 3/10/2025";
+ assertEquals(expectedString, game1.toString(), "The string representation of the game is incorrect.");
+ }
+
+ /**
+ * Tests the equals method to ensure correct equality comparison.
+ */
+ @Test
+ void testEquals() {
+ Date startDate = new Date(125, 3, 10);
+ Date endDate = new Date(125, 3, 15);
+ Game duplicateGame = new Game("Team A", "Team B", startDate, endDate, game1.getTeam1Odd(), game1.getTeam2Odd());
+
+ assertTrue(game1.equals(duplicateGame), "The two games should be equal.");
+ assertFalse(game1.equals(game2), "The two games should not be equal.");
+ }
+
+ /**
+ * Tests the setTeam1 and getTeam1 methods.
+ */
+ @Test
+ void testSetAndGetTeam1() {
+ game1.setTeam1("Team X");
+ assertEquals("Team X", game1.getTeam1(), "Team 1 should be updated to 'Team X'.");
+ }
+
+ /**
+ * Tests the setTeam2 and getTeam2 methods.
+ */
+ @Test
+ void testSetAndGetTeam2() {
+ game1.setTeam2("Team Y");
+ assertEquals("Team Y", game1.getTeam2(), "Team 2 should be updated to 'Team Y'.");
+ }
+
+ /**
+ * Tests the setStartDate and getStartDate methods.
+ */
+ @Test
+ void testSetAndGetStartDate() {
+ Date newStartDate = new Date(2025, 4, 1); // May 1, 2025
+ game1.setStartDate(newStartDate);
+ assertEquals(newStartDate, game1.getStartDate(), "The start date should be updated.");
+ }
+
+ /**
+ * Tests the setEndDate and getEndDate methods.
+ */
+ @Test
+ void testSetAndGetEndDate() {
+ Date newEndDate = new Date(2025, 4, 10); // May 10, 2025
+ game1.setEndDate(newEndDate);
+ assertEquals(newEndDate, game1.getEndDate(), "The end date should be updated.");
+ }
+}
diff --git a/Common/src/test/java/edu/sdccd/cisc191/template/UserTest.java b/Common/src/test/java/edu/sdccd/cisc191/template/UserTest.java
new file mode 100644
index 000000000..640b06759
--- /dev/null
+++ b/Common/src/test/java/edu/sdccd/cisc191/template/UserTest.java
@@ -0,0 +1,159 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the User class.
+ * This test class validates the functionality of the methods in the
+ * User class, including getters, setters, and methods
+ * that manage bets and money attributes.
+ *
+ * @author Andy Ly
+ */
+class UserTest {
+
+ private User user;
+
+ /**
+ * Sets up a test user before each test case.
+ */
+ @BeforeEach
+ void setUp() {
+ user = new User("John Doe", 1000);
+ }
+
+ /**
+ * Tests the constructor to ensure a user is correctly initialized.
+ */
+ @Test
+ void testConstructor() {
+ assertEquals("John Doe", user.getName(), "The user's name should be 'John Doe'.");
+ assertEquals(1000, user.getMoney(), "The user's initial money should be 1000.");
+ assertEquals(0, user.getMoneyLine(), "The user's moneyLine should be initialized to 0.");
+ assertEquals(1000, user.getMoneyBet(), "The user's moneyBet should match the initial money.");
+ assertTrue(user.getBets().isEmpty(), "The user's bets list should be empty upon initialization.");
+ }
+
+ /**
+ * Tests the setName and getName methods.
+ */
+ @Test
+ void testSetAndGetName() {
+ user.setName("Jane Doe");
+ assertEquals("Jane Doe", user.getName(), "The user's name should be updated to 'Jane Doe'.");
+ }
+
+ /**
+ * Tests the setMoney an getMoney methods.
+ */
+ @Test
+ void testSetAndGetMoney() {
+ user.setMoney(500);
+ assertEquals(500, user.getMoney(), "The user's total money should be updated to 500.");
+ }
+
+ /**
+ * Tests the addBet method to ensure bets are added correctly and money attributes are updated.
+ */
+ @Test
+ void testAddBet() {
+ Game game = new Game("Team A", "Team B", new Date(), new Date());
+ Bet bet = new Bet(game, 200, "Team A");
+
+ user.addBet(bet);
+
+ assertEquals(1, user.getBets().size(), "The user's bets list should contain one bet.");
+ assertEquals(800, user.getMoneyBet(), "The user's moneyBet should be reduced by the bet amount.");
+ assertEquals(200, user.getMoneyLine(), "The user's moneyLine should be increased by the bet amount.");
+ }
+
+ /**
+ * Tests the removeBet method to ensure bets are removed correctly.
+ */
+ @Test
+ void testRemoveBet() {
+ Game game = new Game("Team A", "Team B", new Date(), new Date());
+ Bet bet = new Bet(game, 200, "Team A");
+
+ user.addBet(bet);
+ user.removeBet(bet);
+
+ assertTrue(user.getBets().isEmpty(), "The user's bets list should be empty after removing the bet.");
+ }
+
+ /**
+ * Tests the getMoneyLine and getMoneyLine methods.
+ */
+ @Test
+ void testSetAndGetMoneyLine() {
+ user.setMoneyLine(300);
+ assertEquals(300, user.getMoneyLine(), "The user's moneyLine should be updated to 300.");
+ }
+
+ /**
+ * Tests the setMoneyBet and getMoneyBet methods.
+ */
+ @Test
+ void testSetAndGetMoneyBet() {
+ user.setMoneyBet(700);
+ assertEquals(700, user.getMoneyBet(), "The user's moneyBet should be updated to 700.");
+ }
+
+ /**
+ * Tests the checkBet method to verify if an active bet exists for a given game.
+ */
+ @Test
+ void testCheckBet() {
+ Game game1 = new Game("Team A", "Team B", new Date(), new Date());
+ Game game2 = new Game("Team C", "Team D", new Date(), new Date());
+ Bet bet = new Bet(game1, 200, "Team A");
+
+ user.addBet(bet);
+
+ assertTrue(user.checkBet(game1), "The user should have an active bet on 'Team A vs. Team B'.");
+ assertFalse(user.checkBet(game2), "The user should not have an active bet on 'Team C vs. Team D'.");
+ }
+
+ /**
+ * Tests the toJSON method to ensure the User object is serialized correctly.
+ */
+ @Test
+ void testToJSON() throws Exception {
+ String json = User.toJSON(user);
+
+ assertNotNull(json, "The serialized JSON string should not be null.");
+ assertTrue(json.contains("\"name\":\"John Doe\""), "The JSON should contain the user's name.");
+ assertTrue(json.contains("\"money\":1000"), "The JSON should contain the user's money.");
+ }
+
+ /**
+ * Tests the fromJSON method to ensure the User object is deserialized correctly.
+ */
+ @Test
+ void testFromJSON() throws Exception {
+ String json = """
+ {
+ "name": "John Doe",
+ "money": 1000,
+ "moneyLine": 0,
+ "moneyBet": 1000,
+ "bets": []
+ }
+ """;
+
+ User deserializedUser = User.fromJSON(json);
+
+ assertNotNull(deserializedUser, "The deserialized User object should not be null.");
+ assertEquals("John Doe", deserializedUser.getName(), "The deserialized user's name should be 'John Doe'.");
+ assertEquals(1000, deserializedUser.getMoney(), "The deserialized user's money should be 1000.");
+ assertEquals(0, deserializedUser.getMoneyLine(), "The deserialized user's moneyLine should be 0.");
+ assertEquals(1000, deserializedUser.getMoneyBet(), "The deserialized user's moneyBet should be 1000.");
+ assertTrue(deserializedUser.getBets().isEmpty(), "The deserialized user's bets list should be empty.");
+ }
+}
diff --git a/README.md b/README.md
index e7ea67d17..596bb913a 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,9 @@ Intermediate Java Programming Final Project Template
1. Maven
2. Git
3. JDK 21
+4. JavaFX
## Building
-mvn clean install
+mvn clean instal
## Running
java -jar Server/target/Server-1.0.0.jar
java -jar Client/target/Client-1.0.0.jar
@@ -14,4 +15,5 @@ Shared classes between client and server modules.
## Server Module
The server application that handles multiple clients.
## Client Module
-The client application used to connect to the server.
\ No newline at end of file
+The client application used to connect to the server.
+
diff --git a/Server/pom.xml b/Server/pom.xml
index 02b4d93de..d71bd9220 100644
--- a/Server/pom.xml
+++ b/Server/pom.xml
@@ -16,6 +16,17 @@
Common
${project.version}
+
+ org.mockito
+ mockito-core
+ 5.3.1
+ test
+
+
+ com.googlecode.json-simple
+ json-simple
+ 1.1.1
+
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/API/BasketballGetter.java b/Server/src/main/java/edu/sdccd/cisc191/template/API/BasketballGetter.java
new file mode 100644
index 000000000..d795dc426
--- /dev/null
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/API/BasketballGetter.java
@@ -0,0 +1,53 @@
+package edu.sdccd.cisc191.template.API;
+
+import edu.sdccd.cisc191.template.Game;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Objects;
+
+public class BasketballGetter extends APIGetter {
+ public BasketballGetter() {
+ apiURL = "https://v1.basketball.api-sports.io/games?date=";
+ leagueName = "NBA";
+ }
+
+ public static void main(String[] args) throws Exception {
+ BasketballGetter basketballGetter = new BasketballGetter();
+ System.out.println(basketballGetter.getGames());
+ }
+
+ @Override
+ public String sendRequest(URI requestURI) throws Exception {
+ // Create an HttpClient instance
+ HttpClient client = HttpClient.newHttpClient();
+ String apiKey = System.getenv("API_KEY");
+
+ ArrayList bbGames = new ArrayList<>();
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(requestURI) // <-- pass your concatenated URI here
+ .header("x-rapidapi-host", "v1.basketball.api-sports.io")
+ .header("x-rapidapi-key", apiKey)
+ .GET()
+ .build();
+
+ // Make an asynchronous request similar to using JavaScript promises
+ String response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
+ .thenApply(HttpResponse::body)
+ .exceptionally(e -> {
+ System.out.println("Error: " + e.getMessage());
+ return null;
+ })
+ .join().toString(); // Waits for the async call to complete
+
+ return response;
+ }
+
+}
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/APIGetter.java b/Server/src/main/java/edu/sdccd/cisc191/template/APIGetter.java
new file mode 100644
index 000000000..6ff01a274
--- /dev/null
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/APIGetter.java
@@ -0,0 +1,89 @@
+package edu.sdccd.cisc191.template.API;
+
+import java.net.URI;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Objects;
+
+import edu.sdccd.cisc191.template.Game;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+public abstract class APIGetter {
+ String apiURL;
+ String leagueName;
+ public APIGetter() {}
+
+ public String getDateAsString() {
+ Date today = new Date(); // current date
+ LocalDate localDate = today.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+ LocalDate nextDay = localDate.plusDays(1);
+ Date nextDayDate = Date.from(nextDay.atStartOfDay(ZoneId.systemDefault()).toInstant());
+
+ LocalDate tomorrowLocalDate = nextDayDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+
+ String tomorrowArg = tomorrowLocalDate.getYear() + "-0" + tomorrowLocalDate.getMonthValue() + "-0" + tomorrowLocalDate.getDayOfMonth();
+ return tomorrowArg;
+ }
+
+ public ArrayList getGames() throws Exception {
+ String fullUrl = apiURL + getDateAsString();
+ URI requestURI = URI.create(fullUrl);
+ System.out.println(requestURI);
+
+ String response = sendRequest(requestURI);
+
+ JSONParser parser = new JSONParser();
+ JSONObject json = (JSONObject) parser.parse(response);
+
+
+ ArrayList games = parse(json);
+
+ return games;
+ }
+
+ public abstract String sendRequest(URI requestURI) throws Exception;
+
+ public ArrayList parse(JSONObject json) throws ParseException {
+ ArrayList games = new ArrayList<>();
+
+ for (Object keyObj : json.keySet()) {
+ String key = (String) keyObj;
+ Object value = json.get(key);
+
+ System.out.println("Key: " + key + " - Value: " + value);
+
+ // Optional: if the value is a nested JSON array or another JSONObject, you can iterate them too.
+ if (value instanceof JSONArray) {
+ JSONArray array = (JSONArray) value;
+ for (Object item : array) {
+ // Here, you might need to cast item to a JSONObject if that's what it is.
+ if (item instanceof JSONObject) {
+ JSONObject nestedObj = (JSONObject) item;
+ // Process nestedObj here
+ JSONObject league = (JSONObject) nestedObj.get("league");
+ if (Objects.equals(league.get("name").toString(), leagueName)) {
+ JSONObject teams = (JSONObject) nestedObj.get("teams");
+ JSONObject awayTeam = (JSONObject) teams.get("away");
+ JSONObject homeTeam = (JSONObject) teams.get("home");
+ String awayTeamName = awayTeam.get("name").toString();
+ String homeTeamName = homeTeam.get("name").toString();
+
+ System.out.println(awayTeamName + homeTeamName);
+
+ Game newGame = new Game(awayTeamName, homeTeamName, new Date(), 0, 0);
+
+ games.add(newGame);
+ }
+ }
+ }
+ }
+ }
+ return games;
+ }
+};
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/BaseballGetter.java b/Server/src/main/java/edu/sdccd/cisc191/template/BaseballGetter.java
new file mode 100644
index 000000000..3c49b7ad6
--- /dev/null
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/BaseballGetter.java
@@ -0,0 +1,50 @@
+package edu.sdccd.cisc191.template.API;
+
+import edu.sdccd.cisc191.template.Game;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.ParseException;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.ArrayList;
+
+public class BaseballGetter extends APIGetter{
+ public BaseballGetter() {
+ apiURL = "https://v1.baseball.api-sports.io/games?date=";
+ leagueName = "MLB";
+ }
+
+ @Override
+ public String sendRequest(URI requestURI) throws Exception {
+ // Create an HttpClient instance
+ HttpClient client = HttpClient.newHttpClient();
+ String apiKey = System.getenv("API_KEY");
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(requestURI) // <-- pass your concatenated URI here
+ .header("x-rapidapi-host", "v1.baseball.api-sports.io")
+ .header("x-rapidapi-key", apiKey)
+ .GET()
+ .build();
+
+ // Make an asynchronous request similar to using JavaScript promises
+ String response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
+ .thenApply(HttpResponse::body)
+ .exceptionally(e -> {
+ System.out.println("Error: " + e.getMessage());
+ return null;
+ })
+ .join().toString(); // Waits for the async call to complete
+
+ return response;
+
+ }
+
+ public static void main(String[] args) throws Exception {
+ BaseballGetter footballGetter = new BaseballGetter();
+ System.out.println(footballGetter.getGames());
+ }
+
+}
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/ClientHandler.java b/Server/src/main/java/edu/sdccd/cisc191/template/ClientHandler.java
new file mode 100644
index 000000000..15238acad
--- /dev/null
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/ClientHandler.java
@@ -0,0 +1,195 @@
+package edu.sdccd.cisc191.template;
+
+import edu.sdccd.cisc191.template.API.BasketballGetter;
+import edu.sdccd.cisc191.template.API.BaseballGetter;
+
+import java.io.*;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.*;
+
+/**
+ * Handles client requests in a separate thread. Each instance of ClientHandler
+ * is responsible for processing the requests of a single client connected to the server.
+ *
+ * It supports operations such as retrieving game or user details, updating user information,
+ * and managing bets. Communication is facilitated through JSON-encoded requests and responses.
+ *
+ * @author Andy Ly
+ * @see Server
+ * @see Request
+ */
+class ClientHandler implements Runnable {
+
+ private ServerSocket serverSocket; // Server socket for incoming connections
+ private Socket clientSocket; // Socket for communicating with the client
+ ObjectOutputStream out; // Output stream to send responses to the client
+ ObjectInputStream in; // Input stream to receive requests from the client
+
+ /**
+ * Creates a new ClientHandler for a given client socket.
+ *
+ * @param socket The client socket to be handled.
+ */
+ public ClientHandler(Socket socket) throws IOException {
+ this.clientSocket = socket;
+ }
+
+
+ /**
+ * Executes the thread to handle client communication.
+ * Processes incoming JSON-encoded requests, determines their type,
+ * and routes them to the appropriate handler methods.
+ */
+ @Override
+ public void run() {
+ System.out.println("Passed duties on to ClientHandler...");
+
+ try {
+ out = new ObjectOutputStream(clientSocket.getOutputStream());
+ in = new ObjectInputStream(clientSocket.getInputStream());
+
+ while (true) {
+ // Block until the client sends a Request object
+ Object obj = in.readObject();
+ System.out.println("Object Received: \n" + obj);
+ if(!(obj instanceof Request)) {
+ System.err.println("Unexpected type: " + obj.getClass().getName());
+ break;
+ }
+ Request request = (Request) obj;
+ System.out.println("Client received: " + request);
+
+ // Handle the request
+ Object response;
+ switch (request.getRequestType()) {
+ case "GetSize":
+ response = (request.getId() == 1) ? GameDatabase.getInstance().getSize() : UserDatabase.getInstance().getSize();
+ break;
+ case "Game":
+ response = (request.getId() >= 0) ? getGame(request) : null;
+ break;
+ case "User":
+ response = (request.getId() >= 0) ? getUser(request) : null;
+ break;
+ case "ModifyUser":
+ response = (request.getId() >= 0) ? handleModifyUserRequest(request) : null;
+ break;
+ case "Basketball":
+ response = (request.getId() >= 0) ? getBasketball(request) : new ArrayList();
+ break;
+ case "Baseball":
+ response = (request.getId() >= 0) ? getBaseball(request) : new ArrayList();
+ break;
+ default:
+ response = new IllegalArgumentException("Unknown request type");
+ }
+
+ // Send it back
+ out.writeObject(response);
+ out.flush();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ try {
+ clientSocket.close();
+ } catch (IOException ignored) {}
+ }
+ }
+
+ /**
+ * Retrieves the game associated with the given request ID.
+ *
+ * @param request The client request containing the game ID.
+ * @return The Game object corresponding to the ID, or null if not found.
+ */
+ private static Game getGame(Request request) {
+ Game response;
+
+ List gameDatabase = GameDatabase.getInstance().getGameDatabase();
+ if (request.getId() >= gameDatabase.size()) {
+ response = null;
+ } else {
+ response = gameDatabase.get(request.getId());
+ }
+ return response;
+ }
+
+ private static ArrayList getBasketball(Request request) throws Exception {
+ BasketballGetter a = new BasketballGetter();
+ return a.getGames();
+ }
+
+ private static ArrayList getBaseball(Request request) throws Exception {
+ BaseballGetter a = new BaseballGetter();
+ return a.getGames();
+ }
+
+ /**
+ * Retrieves the user associated with the given request ID.
+ *
+ * @param request The client request containing the user ID.
+ * @return The User object corresponding to the ID, or null if not found.
+ */
+ private static User getUser(Request request) {
+ User response;
+
+ List userDatabase = UserDatabase.getInstance().getUserDatabase();
+ if (request.getId() >= userDatabase.size()) {
+ response = null;
+ } else {
+ response = userDatabase.get(request.getId());
+ }
+ return response;
+ }
+
+ /**
+ * Modifies the attributes of a user based on the given request.
+ *
+ * This method is synchronized to ensure thread safety when multiple clients
+ * modify the user database concurrently.
+ *
+ * @param request The client request containing details of the modifications.
+ * @return The modified User object.
+ * @throws Exception If an error occurs during modification.
+ */
+ private static synchronized User handleModifyUserRequest(Request request) throws Exception {
+ UserDatabase db = UserDatabase.getInstance();
+ List userDatabase = db.getUserDatabase();
+
+ User userToModify = userDatabase.get(request.getId());
+
+ // Update user attributes based on the request
+ Map attributes = request.getAttributesToModify();
+ if (attributes.containsKey("Name")) {
+ userToModify.setName((String) attributes.get("Name"));
+ }
+ if (attributes.containsKey("Money")) {
+ userToModify.setMoney((Integer) attributes.get("Money"));
+ }
+
+ if (attributes.containsKey("Increment Money")) {
+ userToModify.incrMoney((Integer) attributes.get("Increment Money"));
+ }
+
+ if (attributes.containsKey("Decrement Money")) {
+ userToModify.incrMoney((Integer) attributes.get("Decrement Money"));
+ }
+
+ if (attributes.containsKey("addBet")) {
+ userToModify.addBet(Bet.fromJSON((String) attributes.get("addBet")));
+ }
+ if (attributes.containsKey("removeBet")) {
+ userToModify.removeBet(Bet.fromJSON((String) attributes.get("removeBet")));
+ }
+
+ db.saveToFile();
+
+ return userToModify;
+ }
+}
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/GameDatabase.java b/Server/src/main/java/edu/sdccd/cisc191/template/GameDatabase.java
new file mode 100644
index 000000000..d19fde88e
--- /dev/null
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/GameDatabase.java
@@ -0,0 +1,147 @@
+package edu.sdccd.cisc191.template;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.CollectionType;
+import org.json.simple.parser.ParseException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Writer;
+import java.io.FileWriter;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * A singleton class that manages a database of games.
+ * It provides functionalities to load, save, and access the database.
+ *
+ * The data is stored in JSON format, and the database is thread-safe
+ * to ensure proper operation in concurrent environments.
+ *
+ * @author Andy Ly
+ */
+public class GameDatabase {
+
+ // Singleton instance
+ private static GameDatabase instance;
+
+ private static final List gameDatabase = Collections.synchronizedList(new ArrayList<>());
+
+ // File path for storing game data
+ private static final URL FILE_PATH = GameDatabase.class.getResource("games.json");
+
+ /**
+ * Private constructor to prevent instantiation outside the class.
+ * Initializes the database by either loading data from the file
+ * or creating a default dataset.
+ */
+ private GameDatabase() {
+ loadOrInitializeDatabase();
+ }
+
+ /**
+ * Retrieves the singleton instance of the GameDatabase class.
+ *
+ * @return The singleton GameDatabase instance.
+ */
+ public static synchronized GameDatabase getInstance() {
+ if (instance == null) {
+ instance = new GameDatabase();
+ }
+ return instance;
+ }
+
+ /**
+ * Loads the game database from a JSON file if it exists, or initializes
+ * it with default data if the file is not found.
+ */
+ void loadOrInitializeDatabase() {
+ File file = new File(FILE_PATH.getFile());
+ if (file.exists()) {
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ // Register subtypes explicitly if necessary (optional if using annotations)
+ CollectionType listType = objectMapper.getTypeFactory()
+ .constructCollectionType(List.class, Game.class);
+ List games = objectMapper.readValue(file, listType);
+
+ gameDatabase.clear();
+ gameDatabase.addAll(games);
+ System.out.println("GameDatabase loaded from file.");
+ //this.updateDatabaseFromAPI();
+ //System.out.println("GameDatabase updated from API.");
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.out.println("Failed to load GameDatabase from file. Initializing with default data.");
+ initializeDefaultGames();
+ saveToFile();
+ } //catch (ParseException e) {
+ //throw new RuntimeException(e);
+ //}
+ } else {
+ System.out.println("GameDatabase file not found. Initializing with default data.");
+ initializeDefaultGames();
+ saveToFile();
+ }
+ }
+ /**
+ * Initializes the game database with default data.
+ */
+ private void initializeDefaultGames() {
+ // To generate a date between now and 2 years from now
+ Date d1 = new Date();
+ Date d2 = new Date(2025, 1, 1);
+ // To generate default team numbers
+ int count = 0;
+ for (int i = 0; i < 6; i++) {
+ Date randomDate = new Date(ThreadLocalRandom.current()
+ .nextLong(d1.getTime(), d2.getTime()));
+ gameDatabase.add(new Game(
+ String.format("Team %d", count),
+ String.format("Team %d", count + 1), new Date(),0, 0));
+ count += 2;
+ }
+ }
+
+ /**
+ * Saves the current state of the game database to a JSON file.
+ */
+ void saveToFile() {
+ try (Writer writer = new FileWriter(FILE_PATH.getFile())) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.writeValue(writer, gameDatabase);
+ System.out.println("GameDatabase saved to file.");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Updates the game database from the API
+ */
+ void updateDatabaseFromAPI() throws ParseException {
+
+ }
+
+ /**
+ * Retrieves an unmodifiable view of the game database.
+ *
+ * @return An unmodifiable List of games.
+ */
+ public synchronized List getGameDatabase() {
+ return Collections.unmodifiableList(gameDatabase);
+ }
+
+ /**
+ * Gets the size of the game database.
+ *
+ * @return The size of the database as an Int .
+ */
+ public synchronized int getSize() {
+ return gameDatabase.size();
+ }
+}
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/Server.java b/Server/src/main/java/edu/sdccd/cisc191/template/Server.java
index e0db64b13..bd296ff5e 100644
--- a/Server/src/main/java/edu/sdccd/cisc191/template/Server.java
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/Server.java
@@ -4,49 +4,62 @@
import java.io.*;
/**
- * This program is a server that takes connection requests on
- * the port specified by the constant LISTENING_PORT. When a
- * connection is opened, the program sends the current time to
- * the connected socket. The program will continue to receive
- * and process connections until it is killed (by a CONTROL-C,
- * for example). Note that this server processes each connection
- * as it is received, rather than creating a separate thread
- * to process the connection.
+ * A multi-threaded server that listens for connection requests on a specified port
+ * and handles each client connection in a separate thread.
+ *
+ * The server sends the current time to connected clients and continues running
+ * indefinitely until it is terminated manually. It uses the {@link ClientHandler}
+ * class to process individual client connections.
+ *
+ * @version 1.0.0
+ * @author Andy Ly
+ * @see ClientHandler
*/
public class Server {
- private ServerSocket serverSocket;
- private Socket clientSocket;
- private PrintWriter out;
- private BufferedReader in;
-
- public void start(int port) throws Exception {
- serverSocket = new ServerSocket(port);
- clientSocket = serverSocket.accept();
- out = new PrintWriter(clientSocket.getOutputStream(), true);
- in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
-
- String inputLine;
- while ((inputLine = in.readLine()) != null) {
- CustomerRequest request = CustomerRequest.fromJSON(inputLine);
- CustomerResponse response = new CustomerResponse(request.getId(), "Jane", "Doe");
- out.println(CustomerResponse.toJSON(response));
- }
- }
- public void stop() throws IOException {
- in.close();
- out.close();
- clientSocket.close();
- serverSocket.close();
- }
+ /**
+ * The entry point of the server application. Sets up the server to listen on
+ * port 4444, accepts client connections, and delegates processing to
+ * ClientHandler instances running in separate threads.
+ *
+ * @param args Command-line arguments (not used in this application).
+ */
public static void main(String[] args) {
- Server server = new Server();
+ ServerSocket serverSocket = null;
+
try {
- server.start(4444);
- server.stop();
- } catch(Exception e) {
+ // Initialize the server to listen on port 4444 with a backlog of 4096.
+ serverSocket = new ServerSocket(4445, 4096);
+
+ System.out.println("Server started on port 4444");
+
+ // Enable address reuse to allow multiple connections from the same host.
+ serverSocket.setReuseAddress(true);
+
+ // Continuously wait for client connections.
+ while (true) {
+ Socket client = serverSocket.accept();
+
+ // Log the new client connection.
+ System.out.println("New client connected: " + client.getInetAddress().getHostAddress());
+
+ // Handle the client connection in a separate thread.
+ ClientHandler clientSocket = new ClientHandler(client);
+ new Thread(clientSocket).start();
+ }
+ } catch (IOException e) {
+ // Print any exceptions that occur during server operation.
e.printStackTrace();
+ } finally {
+ // Ensure the server socket is closed when the server terminates.
+ if (serverSocket != null) {
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
}
-} //end class Server
+}
diff --git a/Server/src/main/java/edu/sdccd/cisc191/template/UserDatabase.java b/Server/src/main/java/edu/sdccd/cisc191/template/UserDatabase.java
new file mode 100644
index 000000000..0394585f1
--- /dev/null
+++ b/Server/src/main/java/edu/sdccd/cisc191/template/UserDatabase.java
@@ -0,0 +1,129 @@
+package edu.sdccd.cisc191.template;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.CollectionType;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * A singleton class that manages a database of users. It provides
+ * functionalities for loading, saving, and accessing user data stored
+ * in JSON format. This class ensures thread-safe operations in
+ * concurrent environments.
+ *
+ * The user data is stored in a synchronized list and can be
+ * initialized with default values or loaded from an existing JSON file.
+ *
+ * @author Andy Ly
+ */
+public class UserDatabase {
+
+ // Singleton instance
+ private static UserDatabase instance;
+
+ // Synchronized list to store user data
+ private static final List userDatabase = Collections.synchronizedList(new ArrayList<>());
+
+ // File path for storing user data
+ private static final URL FILE_PATH = UserDatabase.class.getResource("Users.json");
+
+ /**
+ * Private constructor to prevent instantiation outside the class.
+ * Loads the user data from a JSON file or initializes it with
+ * default values if the file doesn't exist.
+ */
+ private UserDatabase() {
+ loadOrInitializeDatabase();
+ }
+
+ /**
+ * Retrieves the singleton instance of the UserDatabase class.
+ *
+ * @return The singleton UserDatabase instance.
+ */
+ public static synchronized UserDatabase getInstance() {
+ if (instance == null) {
+ instance = new UserDatabase();
+ }
+ return instance;
+ }
+
+ /**
+ * Loads the user database from a JSON file if it exists or initializes
+ * it with default data if the file is not found.
+ */
+ void loadOrInitializeDatabase() {
+ File file = new File(FILE_PATH.getFile());
+ if (file.exists()) {
+ try {
+ ObjectMapper objectMapper = new ObjectMapper();
+ CollectionType listType = objectMapper.getTypeFactory()
+ .constructCollectionType(List.class, User.class);
+ List users = objectMapper.readValue(file, listType);
+ userDatabase.clear();
+ userDatabase.addAll(users);
+ System.out.println("UserDatabase loaded from file.");
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.out.println("Failed to load UserDatabase from file. Initializing with default data.");
+ initializeDefaultUsers();
+ saveToFile();
+ }
+ } else {
+ System.out.println("UserDatabase file not found. Initializing with default data.");
+ initializeDefaultUsers();
+ saveToFile();
+ }
+ }
+
+ /**
+ * Initializes the user database with default data, creating
+ * a list of users with randomized IDs and balances.
+ */
+ private void initializeDefaultUsers() {
+ for (int i = 0; i < 5; i++) {
+ userDatabase.add(new User(
+ UUID.randomUUID().toString().replace("-", "").substring(0, 10),
+ (int) (Math.random() * 1000)));
+ }
+ }
+
+ /**
+ * Saves the current state of the user database to a JSON file.
+ */
+ public void saveToFile() {
+ try (Writer writer = new FileWriter(FILE_PATH.getFile())) {
+ ObjectMapper objectMapper = new ObjectMapper();
+ objectMapper.writeValue(writer, userDatabase);
+ System.out.println("UserDatabase saved to file.");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Retrieves an unmodifiable view of the user database.
+ *
+ * @return An unmodifiable List of users.
+ */
+ public synchronized List getUserDatabase() {
+ return Collections.unmodifiableList(userDatabase);
+ }
+
+ /**
+ * Retrieves the size of the user database.
+ *
+ * @return The size of the database as a String .
+ */
+ public synchronized int getSize() {
+ return userDatabase.size();
+ }
+}
diff --git a/Server/src/main/resources/Users.json b/Server/src/main/resources/Users.json
new file mode 100644
index 000000000..d2ef61405
--- /dev/null
+++ b/Server/src/main/resources/Users.json
@@ -0,0 +1 @@
+[{"name":"61a98ac4e4","money":179,"moneyLine":0,"moneyBet":179,"bets":[]},{"name":"f3e2dc2011","money":716,"moneyLine":0,"moneyBet":716,"bets":[]},{"name":"John","money":9999,"moneyLine":0,"moneyBet":432,"bets":[]},{"name":"7475af3ae7","money":765,"moneyLine":0,"moneyBet":765,"bets":[]},{"name":"141254367d","money":494,"moneyLine":0,"moneyBet":494,"bets":[]}]
\ No newline at end of file
diff --git a/Server/src/main/resources/games.json b/Server/src/main/resources/games.json
new file mode 100644
index 000000000..4e4b5595e
--- /dev/null
+++ b/Server/src/main/resources/games.json
@@ -0,0 +1 @@
+[{"team1":"Team 0","team2":"Team 1","startDate":1745185162963,"endDate":44022473971710,"dateClean":"4/20/2025 - 1/6/3365","team1Wager":0.0,"team2Wager":0.0,"betPool":0.0,"team1PayoutRatio":0.0,"team2PayoutRatio":0.0,"team1ProfitFactor":0.0,"team2ProfitFactor":0.0},{"team1":"Team 2","team2":"Team 3","startDate":1745185162963,"endDate":7129028838844,"dateClean":"4/20/2025 - 11/28/2195","team1Wager":0.0,"team2Wager":0.0,"betPool":0.0,"team1PayoutRatio":0.0,"team2PayoutRatio":0.0,"team1ProfitFactor":0.0,"team2ProfitFactor":0.0},{"team1":"Team 4","team2":"Team 5","startDate":1745185162964,"endDate":5190013391932,"dateClean":"4/20/2025 - 6/19/2134","team1Wager":0.0,"team2Wager":0.0,"betPool":0.0,"team1PayoutRatio":0.0,"team2PayoutRatio":0.0,"team1ProfitFactor":0.0,"team2ProfitFactor":0.0},{"team1":"Team 6","team2":"Team 7","startDate":1745185162964,"endDate":50706732844139,"dateClean":"4/20/2025 - 10/31/3576","team1Wager":0.0,"team2Wager":0.0,"betPool":0.0,"team1PayoutRatio":0.0,"team2PayoutRatio":0.0,"team1ProfitFactor":0.0,"team2ProfitFactor":0.0},{"team1":"Team 8","team2":"Team 9","startDate":1745185162964,"endDate":5044876148700,"dateClean":"4/20/2025 - 11/12/2129","team1Wager":0.0,"team2Wager":0.0,"betPool":0.0,"team1PayoutRatio":0.0,"team2PayoutRatio":0.0,"team1ProfitFactor":0.0,"team2ProfitFactor":0.0},{"team1":"Team 10","team2":"Team 11","startDate":1745185162964,"endDate":7771002441740,"dateClean":"4/20/2025 - 4/2/2216","team1Wager":0.0,"team2Wager":0.0,"betPool":0.0,"team1PayoutRatio":0.0,"team2PayoutRatio":0.0,"team1ProfitFactor":0.0,"team2ProfitFactor":0.0}]
\ No newline at end of file
diff --git a/Server/src/test/java/edu/sdccd/cisc191/template/ClientHandlerTest.java b/Server/src/test/java/edu/sdccd/cisc191/template/ClientHandlerTest.java
new file mode 100644
index 000000000..c318a0753
--- /dev/null
+++ b/Server/src/test/java/edu/sdccd/cisc191/template/ClientHandlerTest.java
@@ -0,0 +1,67 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.*;
+import java.net.Socket;
+
+import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the ClientHandler class.
+ * Validates that client requests are processed correctly and appropriate
+ * responses are sent.
+ */
+class ClientHandlerTest {
+
+ private Socket mockSocket;
+ private PrintWriter writer;
+ private BufferedReader reader;
+ private ClientHandler clientHandler;
+
+ @BeforeEach
+ void setUp() throws IOException {
+ // Mock the socket and its streams
+ mockSocket = mock(Socket.class);
+ writer = new PrintWriter(new StringWriter(), true);
+ reader = new BufferedReader(new StringReader(""));
+
+ when(mockSocket.getOutputStream()).thenReturn(new ByteArrayOutputStream());
+ when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream("".getBytes()));
+
+ clientHandler = new ClientHandler(mockSocket);
+ }
+
+ /**
+ * Tests that the ClientHandler correctly handles a valid request.
+ */
+ @Test
+ void testRunHandlesValidRequest() throws Exception {
+ // Prepare a valid request
+ String mockRequest = """
+ {"requestType": "GetSize", "id": 1, "attributesToModify": {}}
+ """;
+
+ // Simulate receiving the request
+ when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream(mockRequest.getBytes()));
+
+ // Run the handler
+ clientHandler.run();
+
+ // Validate behavior
+ verify(mockSocket, atLeastOnce()).getOutputStream();
+ }
+
+ /**
+ * Tests that the ClientHandler gracefully handles an invalid request.
+ */
+ @Test
+ void testRunHandlesInvalidRequest() throws IOException {
+ // Simulate an invalid request
+ when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream("INVALID_REQUEST".getBytes()));
+
+ assertDoesNotThrow(() -> clientHandler.run(), "ClientHandler should not throw exceptions for invalid requests.");
+ }
+}
diff --git a/Server/src/test/java/edu/sdccd/cisc191/template/GameDatabaseTest.java b/Server/src/test/java/edu/sdccd/cisc191/template/GameDatabaseTest.java
new file mode 100644
index 000000000..6a4929bfb
--- /dev/null
+++ b/Server/src/test/java/edu/sdccd/cisc191/template/GameDatabaseTest.java
@@ -0,0 +1,67 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the GameDatabase class.
+ * Validates loading, saving, and accessing game data.
+ */
+class GameDatabaseTest {
+
+ private static final String TEST_FILE_PATH = "Test/Resources/test_games.json";
+
+ @BeforeEach
+ void setUp() {
+ // Ensure the test file is clean
+ File testFile = new File(TEST_FILE_PATH);
+ if (testFile.exists()) {
+ assertTrue(testFile.delete(), "Test JSON file should be deleted before testing.");
+ }
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Clean up the test file
+ File testFile = new File(TEST_FILE_PATH);
+ if (testFile.exists()) {
+ assertTrue(testFile.delete(), "Test JSON file should be deleted after testing.");
+ }
+ }
+
+ /**
+ * Tests that the database initializes with default values if no file exists.
+ */
+ @Test
+ void testLoadOrInitializeDatabaseWithDefaults() {
+ GameDatabase database = GameDatabase.getInstance();
+
+ // Verify that the database contains default values
+ List games = database.getGameDatabase();
+ assertNotNull(games, "Game database should not be null.");
+ assertFalse(games.isEmpty(), "Game database should contain default values.");
+ }
+
+ /**
+ * Tests saving and loading the database from a JSON file.
+ */
+ @Test
+ void testSaveAndLoadDatabase() {
+ GameDatabase database = GameDatabase.getInstance();
+
+ // Save the current database
+ database.saveToFile();
+
+ // Load the database again
+ database.loadOrInitializeDatabase();
+
+ List games = database.getGameDatabase();
+ assertNotNull(games, "Game database should not be null after reloading.");
+ }
+}
diff --git a/Server/src/test/java/edu/sdccd/cisc191/template/ServerTest.java b/Server/src/test/java/edu/sdccd/cisc191/template/ServerTest.java
new file mode 100644
index 000000000..85b145ca4
--- /dev/null
+++ b/Server/src/test/java/edu/sdccd/cisc191/template/ServerTest.java
@@ -0,0 +1,33 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * Unit tests for the Server class.
+ * Validates behavior when the server interacts with sockets.
+ */
+class ServerTest {
+
+ /**
+ * Tests that the server accepts a connection and delegates handling to ClientHandler .
+ */
+ @Test
+ void testServerAcceptsConnection() throws IOException {
+ // Mock a ServerSocket and a Client Socket
+ ServerSocket mockServerSocket = mock(ServerSocket.class);
+ Socket mockClientSocket = mock(Socket.class);
+
+ // Simulate the server accepting a client connection
+ when(mockServerSocket.accept()).thenReturn(mockClientSocket);
+
+ // Verify that the mock server socket attempts to accept connections
+ mockServerSocket.accept();
+ verify(mockServerSocket, times(1)).accept();
+ }
+}
diff --git a/Server/src/test/java/edu/sdccd/cisc191/template/UserDatabaseTest.java b/Server/src/test/java/edu/sdccd/cisc191/template/UserDatabaseTest.java
new file mode 100644
index 000000000..ac060dbb1
--- /dev/null
+++ b/Server/src/test/java/edu/sdccd/cisc191/template/UserDatabaseTest.java
@@ -0,0 +1,67 @@
+package edu.sdccd.cisc191.template;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the UserDatabase class.
+ * Validates loading, saving, and accessing user data.
+ */
+class UserDatabaseTest {
+
+ private static final String TEST_FILE_PATH = "Test/Resources/test_users.json";
+
+ @BeforeEach
+ void setUp() {
+ // Ensure the test file is clean
+ File testFile = new File(TEST_FILE_PATH);
+ if (testFile.exists()) {
+ assertTrue(testFile.delete(), "Test JSON file should be deleted before testing.");
+ }
+ }
+
+ @AfterEach
+ void tearDown() {
+ // Clean up the test file
+ File testFile = new File(TEST_FILE_PATH);
+ if (testFile.exists()) {
+ assertTrue(testFile.delete(), "Test JSON file should be deleted after testing.");
+ }
+ }
+
+ /**
+ * Tests that the user database initializes with default values if no file exists.
+ */
+ @Test
+ void testLoadOrInitializeDatabaseWithDefaults() {
+ UserDatabase database = UserDatabase.getInstance();
+
+ // Verify that the database contains default values
+ List users = database.getUserDatabase();
+ assertNotNull(users, "User database should not be null.");
+ assertFalse(users.isEmpty(), "User database should contain default values.");
+ }
+
+ /**
+ * Tests saving and loading the user database from a JSON file.
+ */
+ @Test
+ void testSaveAndLoadDatabase() {
+ UserDatabase database = UserDatabase.getInstance();
+
+ // Save the current database
+ database.saveToFile();
+
+ // Load the database again
+ database.loadOrInitializeDatabase();
+
+ List users = database.getUserDatabase();
+ assertNotNull(users, "User database should not be null after reloading.");
+ }
+}
diff --git a/output.json b/output.json
new file mode 100644
index 000000000..77b97b7d1
--- /dev/null
+++ b/output.json
@@ -0,0 +1 @@
+{"response":[{"date":"2025-04-15T00:00:00+00:00","venue":"Coliseo Manuel Petaca Iguina","country":{"code":"PR","flag":"https:\/\/media.api-sports.io\/flags\/pr.svg","name":"Puerto Rico","id":36},"week":null,"teams":{"away":{"name":"Aguada Santeros","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/950.png","id":950},"home":{"name":"Capitanes de Arecibo","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/952.png","id":952}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"BSN","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/76.png","id":76,"type":"League"},"stage":null,"id":443537,"time":"00:00","timestamp":1744675200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T00:00:00+00:00","venue":"Guillermo Angulo Coliseum","country":{"code":"PR","flag":"https:\/\/media.api-sports.io\/flags\/pr.svg","name":"Puerto Rico","id":36},"week":null,"teams":{"away":{"name":"Piratas de Quebradillas","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/956.png","id":956},"home":{"name":"Gigantes de Carolina","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3062.png","id":3062}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"BSN","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/76.png","id":76,"type":"League"},"stage":null,"id":443538,"time":"00:00","timestamp":1744675200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T00:00:00+00:00","venue":null,"country":{"code":"US","flag":"https:\/\/media.api-sports.io\/flags\/us.svg","name":"USA","id":5},"week":"NBA G League - Final","teams":{"away":{"name":"Stockton Kings","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/273.png","id":273},"home":{"name":"Osceola Magic","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/261.png","id":261}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"NBA - G League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/20.png","id":20,"type":"League"},"stage":null,"id":447457,"time":"00:00","timestamp":1744675200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T00:15:00+00:00","venue":"Polideportivo Gran Parque Central","country":{"code":"UY","flag":"https:\/\/media.api-sports.io\/flags\/uy.svg","name":"Uruguay","id":63},"week":"Liga Uruguaya - Quarter-finals","teams":{"away":{"name":"Hebraica y Macabi","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2994.png","id":2994},"home":{"name":"Nacional","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2996.png","id":2996}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Uruguaya","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/110.png","id":110,"type":"League"},"stage":null,"id":447397,"time":"00:15","timestamp":1744676100,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T00:30:00+00:00","venue":"Fortín Rojinegro","country":{"code":"AR","flag":"https:\/\/media.api-sports.io\/flags\/ar.svg","name":"Argentina","id":6},"week":null,"teams":{"away":{"name":"San Lorenzo","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/297.png","id":297},"home":{"name":"San Martin","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/298.png","id":298}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga A","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/18.png","id":18,"type":"League"},"stage":null,"id":436874,"time":"00:30","timestamp":1744677000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T01:00:00+00:00","venue":"Estadio Vicente Rosales","country":{"code":"AR","flag":"https:\/\/media.api-sports.io\/flags\/ar.svg","name":"Argentina","id":6},"week":null,"teams":{"away":{"name":"Obras Sanitarias","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/291.png","id":291},"home":{"name":"Olimpico","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/292.png","id":292}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga A","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/18.png","id":18,"type":"League"},"stage":null,"id":436873,"time":"01:00","timestamp":1744678800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T02:00:00+00:00","venue":"Gimnasio Marcelino Gonzalez","country":{"code":"MX","flag":"https:\/\/media.api-sports.io\/flags\/mx.svg","name":"Mexico","id":28},"week":"LMBPF Women - Semi-finals","teams":{"away":{"name":"Libelulas W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6319.png","id":6319},"home":{"name":"Mineras W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6320.png","id":6320}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"LMBPF Women","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/252.png","id":252,"type":"League"},"stage":null,"id":447458,"time":"02:00","timestamp":1744682400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T03:15:00+00:00","venue":"Arena ITSON","country":{"code":"MX","flag":"https:\/\/media.api-sports.io\/flags\/mx.svg","name":"Mexico","id":28},"week":null,"teams":{"away":{"name":"Halcones de Obregon","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3844.png","id":3844},"home":{"name":"Pioneros de Los Mochis","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3846.png","id":3846}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"CIBACOPA","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/251.png","id":251,"type":"League"},"stage":null,"id":440038,"time":"03:15","timestamp":1744686900,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T07:00:00+00:00","venue":"Pulman Arena","country":{"code":"NZ","flag":"https:\/\/media.api-sports.io\/flags\/nz.svg","name":"New Zealand","id":31},"week":null,"teams":{"away":{"name":"Bay Hawks","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/853.png","id":853},"home":{"name":"Indian Panthers","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7327.png","id":7327}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"NBL","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/66.png","id":66,"type":"League"},"stage":null,"id":430670,"time":"07:00","timestamp":1744700400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T08:00:00+00:00","venue":null,"country":{"code":"PH","flag":"https:\/\/media.api-sports.io\/flags\/ph.svg","name":"Philippines","id":60},"week":null,"teams":{"away":{"name":"Davao Tigers","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7356.png","id":7356},"home":{"name":"Manila Batang Quiapo","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7360.png","id":7360}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"MPBL","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/426.png","id":426,"type":"League"},"stage":null,"id":445529,"time":"08:00","timestamp":1744704000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T09:00:00+00:00","venue":"Sport Concert Complex","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":"1","teams":{"away":{"name":"Spartak Moscow W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1005.png","id":1005},"home":{"name":"Dynamo Kursk W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/997.png","id":997}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/83.png","id":83,"type":"League"},"stage":null,"id":447261,"time":"09:00","timestamp":1744707600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T10:00:00+00:00","venue":null,"country":{"code":"PH","flag":"https:\/\/media.api-sports.io\/flags\/ph.svg","name":"Philippines","id":60},"week":null,"teams":{"away":{"name":"Quezon Huskers","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7371.png","id":7371},"home":{"name":"Pasig City","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7369.png","id":7369}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"MPBL","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/426.png","id":426,"type":"League"},"stage":null,"id":445531,"time":"10:00","timestamp":1744711200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T10:00:00+00:00","venue":"Dongchun Gymnasium","country":{"code":"KR","flag":"https:\/\/media.api-sports.io\/flags\/kr.svg","name":"South Korea","id":44},"week":"KBL - Quarter-finals","teams":{"away":{"name":"Anyang JungKwanJang","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1102.png","id":1102},"home":{"name":"Mobis Phoebus","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1107.png","id":1107}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"KBL","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/91.png","id":91,"type":"League"},"stage":null,"id":447258,"time":"10:00","timestamp":1744711200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T11:00:00+00:00","venue":"Sala Sporturilor Constanta","country":{"code":"RO","flag":"https:\/\/media.api-sports.io\/flags\/ro.svg","name":"Romania","id":38},"week":"Liga National Women - 5th place","teams":{"away":{"name":"Cluj Napoca W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/987.png","id":987},"home":{"name":"CSM Constanta W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/991.png","id":991}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga National W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/79.png","id":79,"type":"League"},"stage":null,"id":447324,"time":"11:00","timestamp":1744714800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T12:00:00+00:00","venue":null,"country":{"code":"PH","flag":"https:\/\/media.api-sports.io\/flags\/ph.svg","name":"Philippines","id":60},"week":null,"teams":{"away":{"name":"Pasay Voyagers","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7368.png","id":7368},"home":{"name":"Paranaque Patriots","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7367.png","id":7367}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"MPBL","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/426.png","id":426,"type":"League"},"stage":null,"id":445530,"time":"12:00","timestamp":1744718400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T12:00:00+00:00","venue":"Zhaksy","country":{"code":"KZ","flag":"https:\/\/media.api-sports.io\/flags\/kz.svg","name":"Kazakhstan","id":25},"week":"National League Women - Semi-finals","teams":{"away":{"name":"Turan W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3816.png","id":3816},"home":{"name":"Okzhetpes W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3813.png","id":3813}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"National League Women","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/397.png","id":397,"type":"League"},"stage":null,"id":446265,"time":"12:00","timestamp":1744718400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T12:00:00+00:00","venue":"Arena Sever","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":"Premier League Women - Semi-finals","teams":{"away":{"name":"UMMC Ekaterinburg W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2246.png","id":2246},"home":{"name":"Enisey Krasnoyarsk W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1000.png","id":1000}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/83.png","id":83,"type":"League"},"stage":null,"id":447092,"time":"12:00","timestamp":1744718400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T12:00:00+00:00","venue":"KSK","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":"1","teams":{"away":{"name":"Novosibirsk W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1004.png","id":1004},"home":{"name":"Nika Siktivkar W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1035.png","id":1035}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/83.png","id":83,"type":"League"},"stage":null,"id":447262,"time":"12:00","timestamp":1744718400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T12:30:00+00:00","venue":"Tbilisi Arena","country":{"code":"GE","flag":"https:\/\/media.api-sports.io\/flags\/ge.svg","name":"Georgia","id":16},"week":"Georgian Cup - Semi-finals","teams":{"away":{"name":"Kutaisi","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/511.png","id":511},"home":{"name":"VSA","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6300.png","id":6300}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Georgian Cup","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/394.png","id":394,"type":"cup"},"stage":null,"id":446268,"time":"12:30","timestamp":1744720200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T13:00:00+00:00","venue":"Sports Arena","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":null,"teams":{"away":{"name":"Irkutsk","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1658.png","id":1658},"home":{"name":"Izhevsk","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2243.png","id":2243}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Super League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/81.png","id":81,"type":"League"},"stage":null,"id":446148,"time":"13:00","timestamp":1744722000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T13:00:00+00:00","venue":"Ivanofeio Sports Arena","country":{"code":"GR","flag":"https:\/\/media.api-sports.io\/flags\/gr.svg","name":"Greece","id":18},"week":"Elite League - Quarter-finals","teams":{"away":{"name":"Proteas Voulas","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5644.png","id":5644},"home":{"name":"Iraklis","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/609.png","id":609}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"A2","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/44.png","id":44,"type":"League"},"stage":null,"id":447508,"time":"13:00","timestamp":1744722000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T13:30:00+00:00","venue":"Palace of Sports","country":{"code":"UA","flag":"https:\/\/media.api-sports.io\/flags\/ua.svg","name":"Ukraine","id":49},"week":"Superleague Women - Semi-finals","teams":{"away":{"name":"Kyiv Basket W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1321.png","id":1321},"home":{"name":"Odessa W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1322.png","id":1322}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superleague W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/107.png","id":107,"type":"League"},"stage":null,"id":447360,"time":"13:30","timestamp":1744723800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T13:45:00+00:00","venue":null,"country":{"code":"LB","flag":"https:\/\/media.api-sports.io\/flags\/lb.svg","name":"Lebanon","id":77},"week":"12","teams":{"away":{"name":"Antranik Beirut","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6356.png","id":6356},"home":{"name":"Antonine","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6355.png","id":6355}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Division 1","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/409.png","id":409,"type":"League"},"stage":null,"id":446972,"time":"13:45","timestamp":1744724700,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T14:00:00+00:00","venue":"MTL Arena","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":null,"teams":{"away":{"name":"Neftyanik Omsk W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1034.png","id":1034},"home":{"name":"Samara W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2250.png","id":2250}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/83.png","id":83,"type":"League"},"stage":null,"id":445787,"time":"14:00","timestamp":1744725600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T14:00:00+00:00","venue":"Orenburzhje Sport Hall","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":"Premier League Women - Semi-finals","teams":{"away":{"name":"MBA Moscow W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1001.png","id":1001},"home":{"name":"Nadezhda W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1002.png","id":1002}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League W","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/83.png","id":83,"type":"League"},"stage":null,"id":447091,"time":"14:00","timestamp":1744725600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T14:30:00+00:00","venue":"Sports hall Astana Academy","country":{"code":"KZ","flag":"https:\/\/media.api-sports.io\/flags\/kz.svg","name":"Kazakhstan","id":25},"week":"National League - Quarter-finals","teams":{"away":{"name":"Atyrau","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/768.png","id":768},"home":{"name":"Astana 2","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1589.png","id":1589}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"National League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/57.png","id":57,"type":"League"},"stage":null,"id":447000,"time":"14:30","timestamp":1744727400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T14:30:00+00:00","venue":null,"country":{"code":"KZ","flag":"https:\/\/media.api-sports.io\/flags\/kz.svg","name":"Kazakhstan","id":25},"week":"National League - Quarter-finals","teams":{"away":{"name":"Kaspiy Aktau","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/769.png","id":769},"home":{"name":"Almaty","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1587.png","id":1587}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"National League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/57.png","id":57,"type":"League"},"stage":null,"id":447365,"time":"14:30","timestamp":1744727400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":"Sportkompleks Olimpiec","country":{"code":"BY","flag":"https:\/\/media.api-sports.io\/flags\/by.svg","name":"Belarus","id":51},"week":null,"teams":{"away":{"name":"Impuls","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1352.png","id":1352},"home":{"name":"Borisfen","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1348.png","id":1348}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/111.png","id":111,"type":"League"},"stage":null,"id":419260,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":null,"country":{"code":"BH","flag":"https:\/\/media.api-sports.io\/flags\/bh.svg","name":"Bahrain","id":56},"week":"15","teams":{"away":{"name":"Al Ittihad","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1043.png","id":1043},"home":{"name":"Al Hala","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2532.png","id":2532}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/122.png","id":122,"type":"League"},"stage":null,"id":440529,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":null,"country":{"code":"BH","flag":"https:\/\/media.api-sports.io\/flags\/bh.svg","name":"Bahrain","id":56},"week":"15","teams":{"away":{"name":"Al Najma","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2535.png","id":2535},"home":{"name":"Al Ahli","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/959.png","id":959}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/122.png","id":122,"type":"League"},"stage":null,"id":440532,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":"Nicos Solomonides Arena","country":{"code":"CY","flag":"https:\/\/media.api-sports.io\/flags\/cy.svg","name":"Cyprus","id":54},"week":"26","teams":{"away":{"name":"Paralimni","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1397.png","id":1397},"home":{"name":"AEL","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1390.png","id":1390}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Division A","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/115.png","id":115,"type":"League"},"stage":null,"id":442562,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":null,"country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"5","teams":{"away":{"name":"Otef Darom","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6692.png","id":6692},"home":{"name":"Maccabi Rehovot","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2739.png","id":2739}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446127,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":"Tbilisi Arena","country":{"code":"GE","flag":"https:\/\/media.api-sports.io\/flags\/ge.svg","name":"Georgia","id":16},"week":"Georgian Cup - Semi-finals","teams":{"away":{"name":"Gurjaani Delta","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5674.png","id":5674},"home":{"name":"TSU","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5238.png","id":5238}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Georgian Cup","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/394.png","id":394,"type":"cup"},"stage":null,"id":446269,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":null,"country":{"code":"GR","flag":"https:\/\/media.api-sports.io\/flags\/gr.svg","name":"Greece","id":18},"week":"Elite League - Quarter-finals","teams":{"away":{"name":"Panerythraikos","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2269.png","id":2269},"home":{"name":"Mykonos","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6176.png","id":6176}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"A2","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/44.png","id":44,"type":"League"},"stage":null,"id":447509,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":null,"country":{"code":"GR","flag":"https:\/\/media.api-sports.io\/flags\/gr.svg","name":"Greece","id":18},"week":"Elite League - Quarter-finals","teams":{"away":{"name":"Vikos","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6179.png","id":6179},"home":{"name":"NE Megaridas","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5572.png","id":5572}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"A2","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/44.png","id":44,"type":"League"},"stage":null,"id":447510,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:00:00+00:00","venue":null,"country":{"code":"GR","flag":"https:\/\/media.api-sports.io\/flags\/gr.svg","name":"Greece","id":18},"week":"Elite League - Quarter-finals","teams":{"away":{"name":"Koroivos","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/600.png","id":600},"home":{"name":"Psychikou","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/604.png","id":604}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"A2","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/44.png","id":44,"type":"League"},"stage":null,"id":447511,"time":"15:00","timestamp":1744729200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:30:00+00:00","venue":"Kedainiu arena","country":{"code":"LT","flag":"https:\/\/media.api-sports.io\/flags\/lt.svg","name":"Lithuania","id":27},"week":null,"teams":{"away":{"name":"Lietkabelis","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/790.png","id":790},"home":{"name":"Nevezis","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/792.png","id":792}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"LKL","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/60.png","id":60,"type":"League"},"stage":null,"id":415464,"time":"15:30","timestamp":1744731000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:30:00+00:00","venue":"Falcon club","country":{"code":"BY","flag":"https:\/\/media.api-sports.io\/flags\/by.svg","name":"Belarus","id":51},"week":null,"teams":{"away":{"name":"Grodno","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1351.png","id":1351},"home":{"name":"Minsk","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1029.png","id":1029}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/111.png","id":111,"type":"League"},"stage":null,"id":419261,"time":"15:30","timestamp":1744731000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:30:00+00:00","venue":null,"country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"5","teams":{"away":{"name":"Safed","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6144.png","id":6144},"home":{"name":"Migdal Haemek","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2740.png","id":2740}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446125,"time":"15:30","timestamp":1744731000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:30:00+00:00","venue":"Motonet Areena","country":{"code":"FI","flag":"https:\/\/media.api-sports.io\/flags\/fi.svg","name":"Finland","id":15},"week":"Korisliiga - Semi-finals","teams":{"away":{"name":"Karhu Basket","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/489.png","id":489},"home":{"name":"Kataja","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/490.png","id":490}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Korisliiga","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/37.png","id":37,"type":"League"},"stage":null,"id":446734,"time":"15:30","timestamp":1744731000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:40:00+00:00","venue":null,"country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"4","teams":{"away":{"name":"Ramat Hasharon","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2735.png","id":2735},"home":{"name":"Maccabi Raanana","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2734.png","id":2734}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446124,"time":"15:40","timestamp":1744731600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T15:40:00+00:00","venue":"Maccabi Sports Hall","country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"4","teams":{"away":{"name":"Ironi Eilat","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/679.png","id":679},"home":{"name":"Maccabi Rishon","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/686.png","id":686}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446126,"time":"15:40","timestamp":1744731600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Hala sportova Ranko","country":{"code":" ","flag":"https:\/\/media.api-sports.io\/flags\/ .svg","name":"Europe","id":55},"week":"27","teams":{"away":{"name":"Spartak Subotica","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1670.png","id":1670},"home":{"name":"Mega Basket","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3161.png","id":3161}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"ABA League","season":2024,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/198.png","id":198,"type":"cup"},"stage":null,"id":407588,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Svyturio arena","country":{"code":"LT","flag":"https:\/\/media.api-sports.io\/flags\/lt.svg","name":"Lithuania","id":27},"week":null,"teams":{"away":{"name":"BC Wolves","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5561.png","id":5561},"home":{"name":"Neptunas","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/791.png","id":791}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"LKL","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/60.png","id":60,"type":"League"},"stage":null,"id":415465,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Sportska dvorana Milos Mrdic","country":{"code":"BA","flag":"https:\/\/media.api-sports.io\/flags\/ba.svg","name":"Bosnia-and-Herzegovina","id":57},"week":"21","teams":{"away":{"name":"Mladi Krajisnik W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3565.png","id":3565},"home":{"name":"Leotar Trebinje W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3564.png","id":3564}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Prvenstvo BiH Women","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/219.png","id":219,"type":"League"},"stage":null,"id":417473,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Oradea Arena","country":{"code":"RO","flag":"https:\/\/media.api-sports.io\/flags\/ro.svg","name":"Romania","id":38},"week":"28","teams":{"away":{"name":"Municipal Galati","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/980.png","id":980},"home":{"name":"CSM Oradea","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/975.png","id":975}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Divizia A","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/78.png","id":78,"type":"League"},"stage":null,"id":420397,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":null,"country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"5","teams":{"away":{"name":"Elitzur Ashkelon","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2722.png","id":2722},"home":{"name":"Hapoel Bnei Kfar Kasem","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6143.png","id":6143}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446121,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":null,"country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"5","teams":{"away":{"name":"Hapoel Kfar Saba","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6598.png","id":6598},"home":{"name":"Elitzur Shomron","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5655.png","id":5655}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446122,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":null,"country":{"code":"IL","flag":"https:\/\/media.api-sports.io\/flags\/il.svg","name":"Israel","id":22},"week":"4","teams":{"away":{"name":"Ironi Nahariya","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/683.png","id":683},"home":{"name":"Elitzur Yavne","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2724.png","id":2724}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Leumit","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/239.png","id":239,"type":"League"},"stage":null,"id":446123,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"STC Tambov","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":null,"teams":{"away":{"name":"University-Ugra","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1018.png","id":1018},"home":{"name":"Tambov","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2801.png","id":2801}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Super League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/81.png","id":81,"type":"League"},"stage":null,"id":446149,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Basketbol Gelisim Merkezi","country":{"code":" ","flag":"https:\/\/media.api-sports.io\/flags\/ .svg","name":"Europe","id":55},"week":"Champions League - Quarter-finals","teams":{"away":{"name":"Nymburk","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/446.png","id":446},"home":{"name":"Galatasaray","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1271.png","id":1271}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Champions League","season":2024,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/202.png","id":202,"type":"cup"},"stage":null,"id":446380,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"City sports Hall","country":{"code":"SK","flag":"https:\/\/media.api-sports.io\/flags\/sk.svg","name":"Slovakia","id":42},"week":"Extraliga - Quarter-finals","teams":{"away":{"name":"Inter Bratislava","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1072.png","id":1072},"home":{"name":"BC Komarno","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3384.png","id":3384}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Extraliga","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/87.png","id":87,"type":"League"},"stage":null,"id":447083,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Sportovni hala Sokol","country":{"code":"CZ","flag":"https:\/\/media.api-sports.io\/flags\/cz.svg","name":"Czech Republic","id":12},"week":"NBL - Quarter-finals","teams":{"away":{"name":"Decin","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/442.png","id":442},"home":{"name":"Srsni Pisek","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/4919.png","id":4919}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"NBL","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/32.png","id":32,"type":"League"},"stage":null,"id":447120,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Sporthall Victoria","country":{"code":"BY","flag":"https:\/\/media.api-sports.io\/flags\/by.svg","name":"Belarus","id":51},"week":"Premier League - Semi-finals","teams":{"away":{"name":"Rguor Minsk","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1787.png","id":1787},"home":{"name":"Brest","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1349.png","id":1349}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/111.png","id":111,"type":"League"},"stage":null,"id":447223,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Dynamo Sports Palace","country":{"code":"RU","flag":"https:\/\/media.api-sports.io\/flags\/ru.svg","name":"Russia","id":39},"week":"Super League - Semi-finals","teams":{"away":{"name":"Khimki M.","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1024.png","id":1024},"home":{"name":"CSKA Moscow 2","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1008.png","id":1008}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Super League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/81.png","id":81,"type":"League"},"stage":null,"id":447274,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:00:00+00:00","venue":"Sportova hala Spisska Nova Ves","country":{"code":"SK","flag":"https:\/\/media.api-sports.io\/flags\/sk.svg","name":"Slovakia","id":42},"week":"Extraliga - Quarter-finals","teams":{"away":{"name":"MBK Handlova","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1074.png","id":1074},"home":{"name":"Spisski Rytieri","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2348.png","id":2348}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Extraliga","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/87.png","id":87,"type":"League"},"stage":null,"id":447331,"time":"16:00","timestamp":1744732800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:30:00+00:00","venue":"Vejlby-Risskov Hallen","country":{"code":"DK","flag":"https:\/\/media.api-sports.io\/flags\/dk.svg","name":"Denmark","id":13},"week":null,"teams":{"away":{"name":"Amager","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/463.png","id":463},"home":{"name":"Bears Academy","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2258.png","id":2258}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Basketligaen","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/34.png","id":34,"type":"League"},"stage":null,"id":442481,"time":"16:30","timestamp":1744734600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:30:00+00:00","venue":"Prince Abdullah al-Faisal Stadium","country":{"code":null,"flag":null,"name":"Asia","id":2},"week":"WASL - Final","teams":{"away":{"name":"Al-Ahli Dubai","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5062.png","id":5062},"home":{"name":"Al Ittihad","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1043.png","id":1043}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"WASL","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/389.png","id":389,"type":"League"},"stage":null,"id":445886,"time":"16:30","timestamp":1744734600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T16:30:00+00:00","venue":"Speiterlinghalle","country":{"code":"DE","flag":"https:\/\/media.api-sports.io\/flags\/de.svg","name":"Germany","id":17},"week":"DBBL Women - Semi-finals","teams":{"away":{"name":"Osnabruck W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3376.png","id":3376},"home":{"name":"Keltern W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/542.png","id":542}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"DBBL Women","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/41.png","id":41,"type":"League"},"stage":null,"id":447461,"time":"16:30","timestamp":1744734600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T17:00:00+00:00","venue":null,"country":{"code":"BH","flag":"https:\/\/media.api-sports.io\/flags\/bh.svg","name":"Bahrain","id":56},"week":"15","teams":{"away":{"name":"Al Muharraq","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2534.png","id":2534},"home":{"name":"Al Manama","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2533.png","id":2533}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/122.png","id":122,"type":"League"},"stage":null,"id":440531,"time":"17:00","timestamp":1744736400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T17:00:00+00:00","venue":"SH Vodova","country":{"code":"CZ","flag":"https:\/\/media.api-sports.io\/flags\/cz.svg","name":"Czech Republic","id":12},"week":"NBL - Quarter-finals","teams":{"away":{"name":"Usti n. Labem","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/452.png","id":452},"home":{"name":"Brno","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/441.png","id":441}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"NBL","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/32.png","id":32,"type":"League"},"stage":null,"id":447121,"time":"17:00","timestamp":1744736400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T17:00:00+00:00","venue":"Kongsberghallen","country":{"code":"NO","flag":"https:\/\/media.api-sports.io\/flags\/no.svg","name":"Norway","id":33},"week":"BLNO - Final","teams":{"away":{"name":"Fyllingen","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/877.png","id":877},"home":{"name":"Kongsberg Miners","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/879.png","id":879}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"BLNO","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/68.png","id":68,"type":"League"},"stage":null,"id":447224,"time":"17:00","timestamp":1744736400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T17:00:00+00:00","venue":"Palestra sportive Sezai Surroi","country":{"code":"XK","flag":"https:\/\/media.api-sports.io\/flags\/xk.svg","name":"Kosovo","id":26},"week":"Superliga Women - Final","teams":{"away":{"name":"Peja W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6121.png","id":6121},"home":{"name":"Bashkimi W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6010.png","id":6010}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superliga Women","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/398.png","id":398,"type":"League"},"stage":null,"id":447431,"time":"17:00","timestamp":1744736400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T17:00:00+00:00","venue":"Sporthalle Charlottenburg","country":{"code":"DE","flag":"https:\/\/media.api-sports.io\/flags\/de.svg","name":"Germany","id":17},"week":"DBBL Women - Semi-finals","teams":{"away":{"name":"Saarlouis W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/544.png","id":544},"home":{"name":"Alba Berlin W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/5559.png","id":5559}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"DBBL Women","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/41.png","id":41,"type":"League"},"stage":null,"id":447462,"time":"17:00","timestamp":1744736400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T17:04:00+00:00","venue":"Lulea Energi Arena","country":{"code":"SE","flag":"https:\/\/media.api-sports.io\/flags\/se.svg","name":"Sweden","id":46},"week":"Basketligan - Quarter-finals","teams":{"away":{"name":"Nassjo","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1203.png","id":1203},"home":{"name":"BC Lulea","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1198.png","id":1198}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Basketligan","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/93.png","id":93,"type":"League"},"stage":null,"id":447463,"time":"17:04","timestamp":1744736640,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T18:00:00+00:00","venue":"Sporthal Boshoven","country":{"code":" ","flag":"https:\/\/media.api-sports.io\/flags\/ .svg","name":"Europe","id":55},"week":null,"teams":{"away":{"name":"Donar Groningen","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/848.png","id":848},"home":{"name":"BAL Weert","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/845.png","id":845}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"BNXT League","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/368.png","id":368,"type":"cup"},"stage":null,"id":402349,"time":"18:00","timestamp":1744740000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T18:00:00+00:00","venue":"Reims Arena","country":{"code":"FR","flag":"https:\/\/media.api-sports.io\/flags\/fr.svg","name":"France","id":4},"week":"34","teams":{"away":{"name":"Caen","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/114.png","id":114},"home":{"name":"Chalons-Reims","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/12.png","id":12}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Pro B","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/8.png","id":8,"type":"League"},"stage":null,"id":406153,"time":"18:00","timestamp":1744740000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T18:00:00+00:00","venue":"PalaBigi","country":{"code":" ","flag":"https:\/\/media.api-sports.io\/flags\/ .svg","name":"Europe","id":55},"week":"Champions League - Quarter-finals","teams":{"away":{"name":"Unicaja","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2340.png","id":2340},"home":{"name":"Reggiana","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/725.png","id":725}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Champions League","season":2024,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/202.png","id":202,"type":"cup"},"stage":null,"id":446382,"time":"18:00","timestamp":1744740000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T18:30:00+00:00","venue":"BMW Park","country":{"code":null,"flag":null,"name":"Europe","id":55},"week":"Euroleague - Semi-finals","teams":{"away":{"name":"Crvena zvezda","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1065.png","id":1065},"home":{"name":"Bayern","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/522.png","id":522}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Euroleague","season":2024,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/120.png","id":120,"type":"cup"},"stage":null,"id":447376,"time":"18:30","timestamp":1744741800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T19:00:00+00:00","venue":"Movistar Arena","country":{"code":null,"flag":null,"name":"Europe","id":55},"week":"Euroleague - Semi-finals","teams":{"away":{"name":"Paris","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/108.png","id":108},"home":{"name":"Real Madrid","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2338.png","id":2338}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Euroleague","season":2024,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/120.png","id":120,"type":"cup"},"stage":null,"id":447377,"time":"19:00","timestamp":1744743600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T19:00:00+00:00","venue":"IR-Vollur","country":{"code":"IS","flag":"https:\/\/media.api-sports.io\/flags\/is.svg","name":"Iceland","id":20},"week":"Premier League - Quarter-finals","teams":{"away":{"name":"Stjarnan","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/652.png","id":652},"home":{"name":"IR Reykjavik","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3172.png","id":3172}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier league","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/48.png","id":48,"type":"League"},"stage":null,"id":447378,"time":"19:00","timestamp":1744743600,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T19:30:00+00:00","venue":"Ungmennafélag Álftaness","country":{"code":"IS","flag":"https:\/\/media.api-sports.io\/flags\/is.svg","name":"Iceland","id":20},"week":"Premier League - Quarter-finals","teams":{"away":{"name":"Njardvik","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/651.png","id":651},"home":{"name":"Alftanes","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3734.png","id":3734}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Premier league","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/48.png","id":48,"type":"League"},"stage":null,"id":447379,"time":"19:30","timestamp":1744745400,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T22:30:00+00:00","venue":"Ginasio da ASCEB","country":{"code":"BR","flag":"https:\/\/media.api-sports.io\/flags\/br.svg","name":"Brazil","id":8},"week":null,"teams":{"away":{"name":"SESI Araraquara W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2240.png","id":2240},"home":{"name":"Cerrado W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7310.png","id":7310}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"LBF W","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/27.png","id":27,"type":"League"},"stage":null,"id":441956,"time":"22:30","timestamp":1744756200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T22:30:00+00:00","venue":"Arena Multiuso Brusque","country":{"code":"BR","flag":"https:\/\/media.api-sports.io\/flags\/br.svg","name":"Brazil","id":8},"week":null,"teams":{"away":{"name":"Osasco","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7344.png","id":7344},"home":{"name":"Brusque","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/7342.png","id":7342}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"CBB","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/425.png","id":425,"type":"League"},"stage":null,"id":445298,"time":"22:30","timestamp":1744756200,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T22:45:00+00:00","venue":"Domo José María Vargas","country":{"code":"VE","flag":"https:\/\/media.api-sports.io\/flags\/ve.svg","name":"Venezuela","id":61},"week":null,"teams":{"away":{"name":"Panteras","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/830.png","id":830},"home":{"name":"Piratas de La Guaira","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/4928.png","id":4928}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superliga","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/275.png","id":275,"type":"League"},"stage":null,"id":444703,"time":"22:45","timestamp":1744757100,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:00:00+00:00","venue":"Ginasio Pedrocao","country":{"code":"BR","flag":"https:\/\/media.api-sports.io\/flags\/br.svg","name":"Brazil","id":8},"week":null,"teams":{"away":{"name":"Flamengo","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/333.png","id":333},"home":{"name":"Franca","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/334.png","id":334}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"NBB","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/26.png","id":26,"type":"League"},"stage":null,"id":426191,"time":"23:00","timestamp":1744758000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:00:00+00:00","venue":"Gimnasio José Beracasa","country":{"code":"VE","flag":"https:\/\/media.api-sports.io\/flags\/ve.svg","name":"Venezuela","id":61},"week":null,"teams":{"away":{"name":"Marinos","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1562.png","id":1562},"home":{"name":"Cocodrilos","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1557.png","id":1557}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superliga","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/275.png","id":275,"type":"League"},"stage":null,"id":444704,"time":"23:00","timestamp":1744758000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:00:00+00:00","venue":"Gimnasio Pedro Elías Belisario Aponte","country":{"code":"VE","flag":"https:\/\/media.api-sports.io\/flags\/ve.svg","name":"Venezuela","id":61},"week":null,"teams":{"away":{"name":"Pioneros","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/2254.png","id":2254},"home":{"name":"Gaiteros del Zulia","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/4905.png","id":4905}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superliga","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/275.png","id":275,"type":"League"},"stage":null,"id":444705,"time":"23:00","timestamp":1744758000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:00:00+00:00","venue":"Gimnasio Ciudad de La Asuncion","country":{"code":"VE","flag":"https:\/\/media.api-sports.io\/flags\/ve.svg","name":"Venezuela","id":61},"week":null,"teams":{"away":{"name":"Spartans","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3989.png","id":3989},"home":{"name":"Guaiqueries","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1559.png","id":1559}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superliga","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/275.png","id":275,"type":"League"},"stage":null,"id":444706,"time":"23:00","timestamp":1744758000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:00:00+00:00","venue":"Domo Olímpico Hugo Chávez","country":{"code":"VE","flag":"https:\/\/media.api-sports.io\/flags\/ve.svg","name":"Venezuela","id":61},"week":null,"teams":{"away":{"name":"Toros de Aragua","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/4906.png","id":4906},"home":{"name":"Llaneros","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/1561.png","id":1561}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Superliga","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/275.png","id":275,"type":"League"},"stage":null,"id":444707,"time":"23:00","timestamp":1744758000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:00:00+00:00","venue":null,"country":{"code":"CA","flag":"https:\/\/media.api-sports.io\/flags\/ca.svg","name":"Canada","id":9},"week":"Super League - Semi-finals","teams":{"away":{"name":"KW Titans","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/356.png","id":356},"home":{"name":"Windsor","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/361.png","id":361}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Super League","season":2024,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/405.png","id":405,"type":"League"},"stage":null,"id":447490,"time":"23:00","timestamp":1744758000,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:30:00+00:00","venue":"Polideportivo Cincuentenario","country":{"code":"AR","flag":"https:\/\/media.api-sports.io\/flags\/ar.svg","name":"Argentina","id":6},"week":null,"teams":{"away":{"name":"Riachuelo","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3114.png","id":3114},"home":{"name":"La Union","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/289.png","id":289}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga A","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/18.png","id":18,"type":"League"},"stage":null,"id":436876,"time":"23:30","timestamp":1744759800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:30:00+00:00","venue":"Estadio Arquitecto Ricardo Etcheverry","country":{"code":"AR","flag":"https:\/\/media.api-sports.io\/flags\/ar.svg","name":"Argentina","id":6},"week":"Liga Femenina Women - Semi-finals","teams":{"away":{"name":"El Talar W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/6607.png","id":6607},"home":{"name":"Ferro W","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/3436.png","id":3436}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"Liga Femenina Women","season":2025,"logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/206.png","id":206,"type":"League"},"stage":null,"id":447269,"time":"23:30","timestamp":1744759800,"status":{"timer":null,"short":"NS","long":"Not Started"}},{"date":"2025-04-15T23:30:00+00:00","venue":"Kia Center","country":{"code":"US","flag":"https:\/\/media.api-sports.io\/flags\/us.svg","name":"USA","id":5},"week":"NBA - Semi-finals","teams":{"away":{"name":"Atlanta Hawks","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/132.png","id":132},"home":{"name":"Orlando Magic","logo":"https:\/\/media.api-sports.io\/basketball\/teams\/153.png","id":153}},"scores":{"away":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null},"home":{"total":null,"quarter_4":null,"quarter_3":null,"quarter_2":null,"quarter_1":null,"over_time":null}},"timezone":"UTC","league":{"name":"NBA","season":"2024-2025","logo":"https:\/\/media.api-sports.io\/basketball\/leagues\/12.png","id":12,"type":"League"},"stage":null,"id":447358,"time":"23:30","timestamp":1744759800,"status":{"timer":null,"short":"NS","long":"Not Started"}}],"get":"games","parameters":{"date":"2025-04-15"},"results":84,"errors":[]}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 24a21d2fc..7f35bd8e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,6 +40,12 @@
${jupiter.version}
test
+
+ org.mockito
+ mockito-core
+ 5.3.1
+ test
+