diff --git a/.gitignore b/.gitignore index 36b741e6..97d3341a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,168 @@ tests/out .idea .DS_Store .* +# Created by https://www.toptal.com/developers/gitignore/api/intellij,maven,java +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,maven,java + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +*/target/* +# End of https://www.toptal.com/developers/gitignore/api/intellij,maven,java + !.gitignore diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..bd84550a --- /dev/null +++ b/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + org.machinecoding + machineCoding-parent + 1.0-SNAPSHOT + pom + + + snakeAndLadder + splitWise + + diff --git a/snakeAndLadder/pom.xml b/snakeAndLadder/pom.xml new file mode 100644 index 00000000..dd698886 --- /dev/null +++ b/snakeAndLadder/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + org.machinecoding + machineCoding-parent + 1.0-SNAPSHOT + + + snakeAndLadder + + + 23 + 23 + UTF-8 + + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + org.mockito + mockito-core + 4.0.0 + test + + + diff --git a/snakeAndLadder/src/main/java/org/machinecoding/Main.java b/snakeAndLadder/src/main/java/org/machinecoding/Main.java new file mode 100644 index 00000000..cf68cb56 --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/Main.java @@ -0,0 +1,41 @@ +package org.machinecoding; + +import org.machinecoding.models.Board; +import org.machinecoding.services.GameController; +import org.machinecoding.models.Dice; +import org.machinecoding.models.Player; +import org.machinecoding.models.teleports.Ladder; +import org.machinecoding.models.teleports.Snake; +import org.machinecoding.models.teleports.TeleporterEntity; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scan = new Scanner(System.in); + int targetCell = 100; + + int numSnakes = scan.nextInt(); + List teleporterList = new ArrayList<>(); + for (int i = 0; i < numSnakes; i++) + teleporterList.add(new Snake(scan.nextInt(), scan.nextInt())); + + int numLadder = scan.nextInt(); + for (int i = 0; i < numLadder; i++) + teleporterList.add(new Ladder(scan.nextInt(), scan.nextInt())); + + int numPlayers = scan.nextInt(); + List playerList = new ArrayList<>(); + for (int i = 0; i < numPlayers; i++) + playerList.add(new Player(scan.next(), 0, targetCell)); + + List diceList = new ArrayList<>(); + diceList.add(new Dice()); + + GameController gameController = new GameController(new Board(targetCell), + teleporterList, playerList, diceList); + gameController.start(); + } +} \ No newline at end of file diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/Board.java b/snakeAndLadder/src/main/java/org/machinecoding/models/Board.java new file mode 100644 index 00000000..64f66bf8 --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/Board.java @@ -0,0 +1,7 @@ +package org.machinecoding.models; + +public class Board { + private int cells; + + public Board(int cells) { this.cells = cells; } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/Dice.java b/snakeAndLadder/src/main/java/org/machinecoding/models/Dice.java new file mode 100644 index 00000000..c8aba8dd --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/Dice.java @@ -0,0 +1,7 @@ +package org.machinecoding.models; + +public class Dice { + public int roll() { + return (int) (Math.random() * 6) + 1; + } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/Player.java b/snakeAndLadder/src/main/java/org/machinecoding/models/Player.java new file mode 100644 index 00000000..b9f54f75 --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/Player.java @@ -0,0 +1,29 @@ +package org.machinecoding.models; + +public class Player { + private final String name; + private int position; + private final int targetPosition; + + public Player(String name, int startPosition, int targetPosition){ + this.name = name; + this.position = startPosition; + this.targetPosition = targetPosition; + } + + public void move(int newPosition){ + this.position = (newPosition > targetPosition) ? position : newPosition; + } + + public boolean isWinner(){ + return (position == targetPosition); + } + + public int getPosition(){ + return position; + } + + public String getName(){ + return name; + } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Ladder.java b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Ladder.java new file mode 100644 index 00000000..896afcee --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Ladder.java @@ -0,0 +1,7 @@ +package org.machinecoding.models.teleports; + +public class Ladder extends TeleporterEntity { + public Ladder(int startCell, int endCell) { + super(startCell, endCell); + } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Snake.java b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Snake.java new file mode 100644 index 00000000..63f45446 --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Snake.java @@ -0,0 +1,7 @@ +package org.machinecoding.models.teleports; + +public class Snake extends TeleporterEntity { + public Snake(int startCell, int endCell) { + super(startCell, endCell); + } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Teleporter.java b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Teleporter.java new file mode 100644 index 00000000..7ce3302f --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/Teleporter.java @@ -0,0 +1,6 @@ +package org.machinecoding.models.teleports; + +public interface Teleporter { + boolean canBeUsed(int currentPosition); + int teleport(); +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/TeleporterEntity.java b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/TeleporterEntity.java new file mode 100644 index 00000000..3e03eb8f --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/models/teleports/TeleporterEntity.java @@ -0,0 +1,29 @@ +package org.machinecoding.models.teleports; + +public class TeleporterEntity implements Teleporter { + private final int startCell; + private final int endCell; + + public TeleporterEntity(int startCell, int endCell){ + this.startCell = startCell; + this.endCell = endCell; + } + + @Override + public boolean canBeUsed(int currentPosition) { + return (startCell == currentPosition); + } + + @Override + public int teleport() { + return endCell; + } + + public int getStartCell() { + return startCell; + } + + public int getEndCell() { + return endCell; + } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/services/GameController.java b/snakeAndLadder/src/main/java/org/machinecoding/services/GameController.java new file mode 100644 index 00000000..04bf1e22 --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/services/GameController.java @@ -0,0 +1,57 @@ +package org.machinecoding.services; + +import org.machinecoding.models.Board; +import org.machinecoding.models.Dice; +import org.machinecoding.models.Player; +import org.machinecoding.models.teleports.TeleporterEntity; +import org.machinecoding.strategy.DiceMovementStrategy; +import org.machinecoding.strategy.GameMovementStrategy; +import org.machinecoding.strategy.TeleporterMovementStrategy; + +import java.util.List; + +public class GameController { + private final Board board; + private final List teleports; + private final List players; + private final List dices; + + private GameMovementStrategy movementStrategy; + + public GameController(Board board, List teleports, List players, List dices) { + this.board = board; + this.teleports = teleports; + this.players = players; + this.dices = dices; + + this.movementStrategy = new GameMovementStrategy(new DiceMovementStrategy(dices), new TeleporterMovementStrategy(teleports)); + } + + public void start() { + while(shouldPerformRound()){ + performOneRound(); + } + } + + private boolean shouldPerformRound() { + return players.stream().filter(p -> !p.isWinner()).count() > 1; + } + + public void performOneRound(){ + for (Player player : players) { + if(!shouldPerformRound()) return; + + int currentPosition = player.getPosition(); + int newPosition = movementStrategy.move(currentPosition); + player.move(newPosition); + + System.out.println(player.getName() + " rolled a " + movementStrategy.getDiceValue() + " and moved from " + + currentPosition + " to " + player.getPosition()); + + if (player.isWinner()) { + System.out.println(player.getName() + " wins the game"); + } + } + } +} + diff --git a/snakeAndLadder/src/main/java/org/machinecoding/strategy/DiceMovementStrategy.java b/snakeAndLadder/src/main/java/org/machinecoding/strategy/DiceMovementStrategy.java new file mode 100644 index 00000000..a01d73de --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/strategy/DiceMovementStrategy.java @@ -0,0 +1,22 @@ +package org.machinecoding.strategy; + +import org.machinecoding.models.Dice; + +import java.util.List; + +public class DiceMovementStrategy implements MovementStrategy { + private final List dices; + + public DiceMovementStrategy(List dices) { + this.dices = dices; + } + + @Override + public int move(int pos) { + int steps = 0; + for (Dice dice : dices) { + steps += dice.roll(); + } + return pos + steps; + } +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/strategy/GameMovementStrategy.java b/snakeAndLadder/src/main/java/org/machinecoding/strategy/GameMovementStrategy.java new file mode 100644 index 00000000..64a6c1af --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/strategy/GameMovementStrategy.java @@ -0,0 +1,34 @@ +package org.machinecoding.strategy; + +public class GameMovementStrategy implements MovementStrategy { + final DiceMovementStrategy diceMovementStrategy; + final TeleporterMovementStrategy teleporterMovementStrategy; + + private int diceValue = 0; + private int teleporterValue = 0; + + public GameMovementStrategy(DiceMovementStrategy diceMovementStrategy, + TeleporterMovementStrategy teleporterMovementStrategy) { + this.diceMovementStrategy = diceMovementStrategy; + this.teleporterMovementStrategy = teleporterMovementStrategy; + } + + @Override + public int move(int pos) { + int diceMovedPosition = diceMovementStrategy.move(pos); + this.diceValue = diceMovedPosition - pos; + + int teleporterMovedPosition = teleporterMovementStrategy.move(diceMovedPosition); + this.teleporterValue = teleporterMovedPosition - diceMovedPosition; + + return teleporterMovedPosition; + } + + public int getDiceValue() { + return diceValue; + } + public int getTeleporterValue() { + return teleporterValue; + } + +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/strategy/MovementStrategy.java b/snakeAndLadder/src/main/java/org/machinecoding/strategy/MovementStrategy.java new file mode 100644 index 00000000..f7931c34 --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/strategy/MovementStrategy.java @@ -0,0 +1,6 @@ +package org.machinecoding.strategy; + +public interface MovementStrategy { + int move(int pos); + +} diff --git a/snakeAndLadder/src/main/java/org/machinecoding/strategy/TeleporterMovementStrategy.java b/snakeAndLadder/src/main/java/org/machinecoding/strategy/TeleporterMovementStrategy.java new file mode 100644 index 00000000..58d537ea --- /dev/null +++ b/snakeAndLadder/src/main/java/org/machinecoding/strategy/TeleporterMovementStrategy.java @@ -0,0 +1,37 @@ +package org.machinecoding.strategy; + +import org.machinecoding.models.teleports.TeleporterEntity; + +import java.util.List; + +public class TeleporterMovementStrategy implements MovementStrategy { + private final List teleports; + + public TeleporterMovementStrategy(List teleports) { + this.teleports = teleports; + } + + @Override + public int move(int pos) { + int currentPosition = pos; + + while (true) { + boolean teleported = false; + + for (TeleporterEntity teleport : teleports) { + if (teleport.canBeUsed(currentPosition)) { + currentPosition = teleport.teleport(); + teleported = true; + break; + } + } + + if (!teleported) { + break; + } + } + + return currentPosition; + } + +} diff --git a/snakeAndLadder/src/test/java/org/machinecoding/strategy/DiceMovementStrategyTest.java b/snakeAndLadder/src/test/java/org/machinecoding/strategy/DiceMovementStrategyTest.java new file mode 100644 index 00000000..9e21c5b6 --- /dev/null +++ b/snakeAndLadder/src/test/java/org/machinecoding/strategy/DiceMovementStrategyTest.java @@ -0,0 +1,77 @@ +package org.machinecoding.strategy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.machinecoding.models.Dice; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +class DiceMovementStrategyTest { + + private DiceMovementStrategy diceMovementStrategy; + private List mockDices; + + @BeforeEach + void setUp() { + mockDices = List.of(Mockito.mock(Dice.class), Mockito.mock(Dice.class)); + diceMovementStrategy = new DiceMovementStrategy(mockDices); + } + + // Test Scenario: Test the DiceMovementStrategy class to ensure it correctly implements the MovementStrategy interface, + // focusing on the move method's ability to calculate the new position based on dice rolls. + @ParameterizedTest + @CsvSource({ + "0, 3, 4, 7", + "5, 2, 6, 13", + "10, 1, 1, 12" + }) + void testMoveMethodCalculatesNewPosition(int initialPosition, int roll1, int roll2, int expectedPosition) { + when(mockDices.get(0).roll()).thenReturn(roll1); + when(mockDices.get(1).roll()).thenReturn(roll2); + + int newPosition = diceMovementStrategy.move(initialPosition); + + assertEquals(expectedPosition, newPosition); + } + + // Test Scenario: Test the move method with an empty list of dice to ensure it returns the initial position without changes. + @Test + void testMoveMethodWithEmptyDiceList() { + DiceMovementStrategy strategyWithNoDice = new DiceMovementStrategy(List.of()); + int initialPosition = 10; + + int newPosition = strategyWithNoDice.move(initialPosition); + + assertEquals(initialPosition, newPosition); + } + + // Test Scenario: Test the move method with maximum dice roll values to ensure it calculates the correct new position. + @Test + void testMoveMethodWithMaximumDiceRolls() { + when(mockDices.get(0).roll()).thenReturn(6); + when(mockDices.get(1).roll()).thenReturn(6); + int initialPosition = 0; + + int newPosition = diceMovementStrategy.move(initialPosition); + + assertEquals(12, newPosition); + } + + // Test Scenario: Test the move method with minimum dice roll values to ensure it calculates the correct new position. + @Test + void testMoveMethodWithMinimumDiceRolls() { + when(mockDices.get(0).roll()).thenReturn(1); + when(mockDices.get(1).roll()).thenReturn(1); + int initialPosition = 0; + + int newPosition = diceMovementStrategy.move(initialPosition); + + assertEquals(2, newPosition); + } +} \ No newline at end of file diff --git a/snakeAndLadder/src/test/java/org/machinecoding/strategy/GameMovementStrategyTest.java b/snakeAndLadder/src/test/java/org/machinecoding/strategy/GameMovementStrategyTest.java new file mode 100644 index 00000000..f9c0ad8c --- /dev/null +++ b/snakeAndLadder/src/test/java/org/machinecoding/strategy/GameMovementStrategyTest.java @@ -0,0 +1,202 @@ +package org.machinecoding.strategy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +class GameMovementStrategyTest { + + private DiceMovementStrategy diceMovementStrategy; + private TeleporterMovementStrategy teleporterMovementStrategy; + private GameMovementStrategy gameMovementStrategy; + + @BeforeEach + void setUp() { + diceMovementStrategy = Mockito.mock(DiceMovementStrategy.class); + teleporterMovementStrategy = Mockito.mock(TeleporterMovementStrategy.class); + gameMovementStrategy = new GameMovementStrategy(diceMovementStrategy, teleporterMovementStrategy); + } + + // Test Scenario: Test the move method to ensure it correctly calculates the new position using both dice and teleporter strategies, and updates diceValue and teleporterValue accordingly. + @Test + void testMoveMethodCalculatesNewPositionAndUpdatesValues() { + int initialPosition = 0; + when(diceMovementStrategy.move(initialPosition)).thenReturn(5); + when(teleporterMovementStrategy.move(5)).thenReturn(10); + + int finalPosition = gameMovementStrategy.move(initialPosition); + + assertEquals(10, finalPosition); + assertEquals(5, gameMovementStrategy.getDiceValue()); + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Test if the GameMovementStrategy correctly utilizes the DiceMovementStrategy to calculate the diceValue when move is called. + @Test + void testDiceValueCalculation() { + int initialPosition = 2; + when(diceMovementStrategy.move(initialPosition)).thenReturn(7); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getDiceValue()); + } + + // Test Scenario: Test the behavior of GameMovementStrategy when teleporterMovementStrategy returns a position that is significantly different from the diceMovementStrategy's result, ensuring teleporterValue is calculated correctly. + @Test + void testTeleporterValueWithSignificantDifference() { + int initialPosition = 3; + when(diceMovementStrategy.move(initialPosition)).thenReturn(8); + when(teleporterMovementStrategy.move(8)).thenReturn(20); + + gameMovementStrategy.move(initialPosition); + + assertEquals(12, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Verify that the diceValue is correctly updated after the move method is called, reflecting the difference between the initial position and the position after the dice movement strategy is applied. + @Test + void testDiceValueUpdateAfterMove() { + int initialPosition = 4; + when(diceMovementStrategy.move(initialPosition)).thenReturn(9); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getDiceValue()); + } + + // Test Scenario: Test teleporterValue calculation when teleporterMovementStrategy moves the position forward. + @Test + void testTeleporterValueCalculationForwardMove() { + int initialPosition = 5; + when(diceMovementStrategy.move(initialPosition)).thenReturn(10); + when(teleporterMovementStrategy.move(10)).thenReturn(15); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Test the constructor to ensure it correctly initializes the GameMovementStrategy with given DiceMovementStrategy and TeleporterMovementStrategy instances. + @Test + void testConstructorInitialization() { + GameMovementStrategy strategy = new GameMovementStrategy(diceMovementStrategy, teleporterMovementStrategy); + assertEquals(diceMovementStrategy, strategy.diceMovementStrategy); + assertEquals(teleporterMovementStrategy, strategy.teleporterMovementStrategy); + } + + // Test Scenario: Verify that the GameMovementStrategy constructor correctly assigns the provided DiceMovementStrategy instance to the diceMovementStrategy field, ensuring that subsequent calls to move() use this instance for dice movement calculations. + @Test + void testDiceMovementStrategyAssignmentInConstructor() { + GameMovementStrategy strategy = new GameMovementStrategy(diceMovementStrategy, teleporterMovementStrategy); + assertEquals(diceMovementStrategy, strategy.diceMovementStrategy); + } + + // Test Scenario: Verify that the teleporterMovementStrategy correctly updates the teleporterValue after moving from the diceMovedPosition, ensuring the teleporterValue reflects the difference between the teleporterMovedPosition and diceMovedPosition. + @Test + void testTeleporterMovementStrategyUpdatesTeleporterValue() { + int initialPosition = 6; + when(diceMovementStrategy.move(initialPosition)).thenReturn(11); + when(teleporterMovementStrategy.move(11)).thenReturn(16); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Test the move method with a starting position of 0, ensuring the dice and teleporter strategies are called correctly, and the final position is calculated accurately. + @Test + void testMoveMethodWithStartingPositionZero() { + int initialPosition = 0; + when(diceMovementStrategy.move(initialPosition)).thenReturn(3); + when(teleporterMovementStrategy.move(3)).thenReturn(8); + + int finalPosition = gameMovementStrategy.move(initialPosition); + + assertEquals(8, finalPosition); + } + + // Test Scenario: Test the move method to ensure it correctly updates diceValue when diceMovementStrategy returns a positive move. + @Test + void testDiceValueUpdateWithPositiveMove() { + int initialPosition = 7; + when(diceMovementStrategy.move(initialPosition)).thenReturn(12); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getDiceValue()); + } + + // Test Scenario: Verify that the diceValue is correctly updated when the move method is called, ensuring it reflects the difference between the new position after dice movement and the initial position. + @Test + void testDiceValueCorrectUpdateAfterMove() { + int initialPosition = 8; + when(diceMovementStrategy.move(initialPosition)).thenReturn(13); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getDiceValue()); + } + + // Test Scenario: Test teleporter movement when dice movement results in a position that triggers a teleportation event. + @Test + void testTeleporterMovementOnDiceTrigger() { + int initialPosition = 9; + when(diceMovementStrategy.move(initialPosition)).thenReturn(14); + when(teleporterMovementStrategy.move(14)).thenReturn(19); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Test teleporterValue calculation when teleporterMovementStrategy moves forward by 5 positions from a diceMovedPosition of 10. + @Test + void testTeleporterValueCalculationWithForwardMove() { + int initialPosition = 10; + when(diceMovementStrategy.move(initialPosition)).thenReturn(15); + when(teleporterMovementStrategy.move(15)).thenReturn(20); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Verify that the move method correctly updates the teleporterMovedPosition based on the teleporterMovementStrategy's move result, ensuring the teleporterValue reflects the difference between the teleporterMovedPosition and diceMovedPosition. + @Test + void testTeleporterMovedPositionUpdate() { + int initialPosition = 11; + when(diceMovementStrategy.move(initialPosition)).thenReturn(16); + when(teleporterMovementStrategy.move(16)).thenReturn(21); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } + + // Test Scenario: Verify that the getDiceValue() method returns the correct dice movement value after a move operation is performed. + @Test + void testGetDiceValueMethod() { + int initialPosition = 12; + when(diceMovementStrategy.move(initialPosition)).thenReturn(17); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getDiceValue()); + } + + // Test Scenario: Verify that getTeleporterValue() returns the correct teleporter movement value after move() is called. + @Test + void testGetTeleporterValueMethod() { + int initialPosition = 13; + when(diceMovementStrategy.move(initialPosition)).thenReturn(18); + when(teleporterMovementStrategy.move(18)).thenReturn(23); + + gameMovementStrategy.move(initialPosition); + + assertEquals(5, gameMovementStrategy.getTeleporterValue()); + } +} \ No newline at end of file diff --git a/splitWise/pom.xml b/splitWise/pom.xml new file mode 100644 index 00000000..25abd6db --- /dev/null +++ b/splitWise/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + org.machinecoding + machineCoding-parent + 1.0-SNAPSHOT + + + splitWise + + + 23 + 23 + UTF-8 + + + \ No newline at end of file diff --git a/splitWise/src/main/java/org/machinecoding/Constants.java b/splitWise/src/main/java/org/machinecoding/Constants.java new file mode 100644 index 00000000..9aec1377 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/Constants.java @@ -0,0 +1,10 @@ +package org.machinecoding; + +public class Constants { + public static final String SHOW = "SHOW"; + public static final String EXPENSE = "EXPENSE"; + + public static final String EQUAL = "EQUAL"; + public static final String EXACT = "EXACT"; + public static final String PERCENTAGE = "PERCENT"; +} diff --git a/splitWise/src/main/java/org/machinecoding/Main.java b/splitWise/src/main/java/org/machinecoding/Main.java new file mode 100644 index 00000000..68326308 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/Main.java @@ -0,0 +1,97 @@ +package org.machinecoding; + +import org.machinecoding.models.Expense; +import org.machinecoding.models.User; +import org.machinecoding.services.SplitWiseRunner; +import org.machinecoding.strategy.EqualSplittingStrategy; +import org.machinecoding.strategy.ExactSplittingStrategy; +import org.machinecoding.strategy.PercentageSplittingStrategy; +import org.machinecoding.strategy.SplittingStrategy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Scanner; + +import static org.machinecoding.Constants.*; + +public class Main { + public static void main(String[] args) { + User user1 = new User("u1", "user1", "user1@gmail.com", "XXXXXXX"); + User user2 = new User("u2", "user2", "user2@gmail.com", "XXXXXXX"); + User user3 = new User("u3", "user3", "user3@gmail.com", "XXXXXXX"); + User user4 = new User("u4", "user4", "user3@gmail.com", "XXXXXXX"); + + SplitWiseRunner runner = new SplitWiseRunner(); + runner.addUser(user1); + runner.addUser(user2); + runner.addUser(user3); + runner.addUser(user4); + + Scanner scan = new Scanner(System.in); + while (scan.hasNextLine()) { + String command = scan.nextLine(); + processCommand(command, runner); + } + scan.close(); + } + + private static void processCommand(String command, SplitWiseRunner runner) { + String[] parts = command.split(" "); + String action = parts[0]; + + switch (action) { + case SHOW: + handleShowCommand(runner, parts); + break; + case EXPENSE: + handleExpenseCommand(runner, parts); + break; + default: + break; + } + } + + private static void handleExpenseCommand(SplitWiseRunner runner, String[] parts) { + String paidBy = parts[1]; + Double amount = Double.parseDouble(parts[2]); + + int len = Integer.parseInt(parts[3]); + List paidFor = new ArrayList<>(Arrays.asList(parts).subList(4, len + 4)); + + String expenseType = parts[len + 4]; + Expense expense = runner.createExpense(amount, getSplittingStrategy(expenseType, len, parts)); + runner.addTransaction(paidBy, paidFor, expense); + } + + private static SplittingStrategy getSplittingStrategy(String expenseType, int numUsers, String[] parts) { + switch (expenseType){ + case EQUAL: + return new EqualSplittingStrategy(numUsers); + case EXACT: + List splitAmt = new ArrayList<>(); + for(int i = 0; i < numUsers; i++) { + splitAmt.add(Double.parseDouble(parts[numUsers + 5 + i])); + } + return new ExactSplittingStrategy(splitAmt); + case PERCENTAGE: + List splitPercentage = new ArrayList<>(); + for(int i = 0; i < numUsers; i++) { + splitPercentage.add(Double.parseDouble(parts[numUsers + 5 + i])); + } + return new PercentageSplittingStrategy(splitPercentage); + default: + break; + } + + return null; + } + + private static void handleShowCommand(SplitWiseRunner runner, String[] parts) { + if (parts.length == 1) { + runner.getAllBalances(); + } else { + runner.getUserBalance(parts[1]); + } + } +} \ No newline at end of file diff --git a/splitWise/src/main/java/org/machinecoding/models/Expense.java b/splitWise/src/main/java/org/machinecoding/models/Expense.java new file mode 100644 index 00000000..035d9426 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/models/Expense.java @@ -0,0 +1,21 @@ +package org.machinecoding.models; + +import org.machinecoding.strategy.SplittingStrategy; + +public class Expense { + private Double amount; + private SplittingStrategy strategy; + + public Expense(Double amount, SplittingStrategy strategy) { + this.amount = amount; + this.strategy = strategy; + } + + public SplittingStrategy getStrategy() { + return strategy; + } + + public Double getAmount() { + return amount; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/models/Transaction.java b/splitWise/src/main/java/org/machinecoding/models/Transaction.java new file mode 100644 index 00000000..e0d1caae --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/models/Transaction.java @@ -0,0 +1,43 @@ +package org.machinecoding.models; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Transaction { + private User paidBy; + private List paidFor; + private Expense expense; + + private Map splits; // paidFor, Expense expense) { + this.paidBy = paidBy; + this.paidFor = paidFor; + this.expense = expense; + this.splits = new HashMap<>(); + } + + public void populateSplitMap() { + List splitAmount = expense.getStrategy().getSplit(expense.getAmount()); + for (int i = 0; i < paidFor.size(); i++) { + splits.put(paidFor.get(i), splitAmount.get(i)); + } + } + + public Map getSplits() { + return splits; + } + + public User getPaidBy() { + return paidBy; + } + + public List getPaidFor() { + return paidFor; + } + + public Expense getExpense() { + return expense; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/models/User.java b/splitWise/src/main/java/org/machinecoding/models/User.java new file mode 100644 index 00000000..9f053a34 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/models/User.java @@ -0,0 +1,31 @@ +package org.machinecoding.models; + +public class User { + private final String id; + private final String name; + private final String email; + private final String mobileNumber; + + public User(String id, String name, String email, String mobileNumber) { + this.mobileNumber = mobileNumber; + this.email = email; + this.name = name; + this.id = id; + } + + public String getMobileNumber() { + return mobileNumber; + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/models/UserPair.java b/splitWise/src/main/java/org/machinecoding/models/UserPair.java new file mode 100644 index 00000000..78efe1eb --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/models/UserPair.java @@ -0,0 +1,35 @@ +package org.machinecoding.models; + +import java.util.Objects; + +public class UserPair { + private User user; + private User friend; + + public User getUser() { + return user; + } + + public User getFriend() { + return friend; + } + + public UserPair(User user, User friend) { + this.user = user; + this.friend = friend; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UserPair userPair = (UserPair) o; + return Objects.equals(user, userPair.user) && + Objects.equals(friend, userPair.friend); + } + + @Override + public int hashCode() { + return Objects.hash(user, friend); + } +} diff --git a/splitWise/src/main/java/org/machinecoding/repository/BalanceRepository.java b/splitWise/src/main/java/org/machinecoding/repository/BalanceRepository.java new file mode 100644 index 00000000..f42c3bee --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/repository/BalanceRepository.java @@ -0,0 +1,80 @@ +package org.machinecoding.repository; + +import org.machinecoding.models.User; +import org.machinecoding.models.UserPair; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class BalanceRepository { + private Map userBalanceMap; + // a b (a -> b) => a owes b + + public BalanceRepository() { + this.userBalanceMap = new HashMap<>(); + } + + public void addUserBalance(UserPair pair, Double balance) { + balance = roundToTwoDecimalPlaces(balance); + + if (Objects.equals(pair.getFriend(), pair.getUser())) return; + if (userBalanceMap.containsKey(pair)) { + userBalanceMap.put(pair, userBalanceMap.get(pair) + balance); + } else { + userBalanceMap.put(pair, balance); + } + + // simplify transactions + UserPair pair2 = new UserPair(pair.getFriend(), pair.getUser()); + if (userBalanceMap.containsKey(pair2)) { + Double amt1 = userBalanceMap.get(pair); + Double amt2 = userBalanceMap.get(pair2); + + if (amt1 > amt2) { + userBalanceMap.put(pair, amt1 - amt2); + userBalanceMap.remove(pair2); + } else if (amt1 < amt2) { + userBalanceMap.put(pair2, amt2 - amt1); + userBalanceMap.remove(pair); + } else { + userBalanceMap.remove(pair); + userBalanceMap.remove(pair2); + } + } + } + + private Double roundToTwoDecimalPlaces(Double value) { + return Math.round(value * 100.0) / 100.0; + } + + public void getUserBalance(User user) { + boolean gotTransaction = false; + for (Map.Entry entry : userBalanceMap.entrySet()) { + User usr = entry.getKey().getUser(); + User frnd = entry.getKey().getFriend(); + if (Objects.equals(usr, user) || Objects.equals(frnd, user)) { + System.out.println(usr.getName() + " owes " + frnd.getName() + ": " + entry.getValue()); + gotTransaction = true; + } + } + + if (!gotTransaction) { + System.out.println("No balances"); + } + } + + public void getAllBalances() { + boolean gotTransaction = false; + for (Map.Entry entry : userBalanceMap.entrySet()) { + User usr = entry.getKey().getUser(); + User frnd = entry.getKey().getFriend(); + System.out.println(usr.getName() + " owes " + frnd.getName() + ": " + entry.getValue()); + gotTransaction = true; + } + + if (!gotTransaction) { + System.out.println("No balances"); + } + } +} diff --git a/splitWise/src/main/java/org/machinecoding/repository/UserRepository.java b/splitWise/src/main/java/org/machinecoding/repository/UserRepository.java new file mode 100644 index 00000000..c4450dee --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/repository/UserRepository.java @@ -0,0 +1,22 @@ +package org.machinecoding.repository; + +import org.machinecoding.models.User; + +import java.util.HashMap; +import java.util.Map; + +public class UserRepository { + private Map userMap; + + public UserRepository() { + userMap = new HashMap<>(); + } + + public void addUser(User user) { + userMap.put(user.getId(), user); + } + + public Map getUserMap() { + return userMap; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/services/SplitWiseRunner.java b/splitWise/src/main/java/org/machinecoding/services/SplitWiseRunner.java new file mode 100644 index 00000000..140cfe17 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/services/SplitWiseRunner.java @@ -0,0 +1,56 @@ +package org.machinecoding.services; + +import org.machinecoding.models.*; +import org.machinecoding.repository.BalanceRepository; +import org.machinecoding.repository.UserRepository; +import org.machinecoding.strategy.SplittingStrategy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class SplitWiseRunner { + private BalanceRepository balanceRepository; + private UserRepository userRepository; + + public SplitWiseRunner(){ + this.balanceRepository = new BalanceRepository(); + this.userRepository = new UserRepository(); + } + + public void addUser(User user){ + userRepository.addUser(user); + } + + public Expense createExpense(Double amt, SplittingStrategy strategy){ + return new Expense(amt, strategy); + } + + public void addTransaction(String paidBy, List paidFor, Expense exp){ + User paidByUser = userRepository.getUserMap().get(paidBy); + List paidForUsers = paidFor.stream() + .map(u->userRepository.getUserMap().get(u)) + .collect(Collectors.toList()); + + Transaction transaction = new Transaction(paidByUser, paidForUsers, exp); + transaction.populateSplitMap(); + + addEntryInUserBalance(paidByUser, transaction); + } + + public void addEntryInUserBalance(User paidBy, Transaction transaction) { + for (Map.Entry entry : transaction.getSplits().entrySet()) { + balanceRepository.addUserBalance(new UserPair(entry.getKey(), paidBy), entry.getValue()); + } + } + + public void getAllBalances() { + balanceRepository.getAllBalances(); + } + + public void getUserBalance(String userId) { + balanceRepository.getUserBalance(userRepository.getUserMap().get(userId)); + } + +} diff --git a/splitWise/src/main/java/org/machinecoding/strategy/EqualSplittingStrategy.java b/splitWise/src/main/java/org/machinecoding/strategy/EqualSplittingStrategy.java new file mode 100644 index 00000000..5e340d98 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/strategy/EqualSplittingStrategy.java @@ -0,0 +1,26 @@ +package org.machinecoding.strategy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class EqualSplittingStrategy implements SplittingStrategy { + private final int numUsers; + + public EqualSplittingStrategy(int numUsers) { + this.numUsers = numUsers; + } + + @Override + public List getSplit(Double amount) { + List share = new ArrayList<>(); + for (int i = 0; i < numUsers; i++) { + share.add(roundToTwoDecimalPlaces(amount / numUsers)); + } + return share; + } + + private Double roundToTwoDecimalPlaces(Double value) { + return Math.round(value * 100.0) / 100.0; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/strategy/ExactSplittingStrategy.java b/splitWise/src/main/java/org/machinecoding/strategy/ExactSplittingStrategy.java new file mode 100644 index 00000000..2ebf5c29 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/strategy/ExactSplittingStrategy.java @@ -0,0 +1,32 @@ +package org.machinecoding.strategy; + +import java.util.List; +import java.util.stream.Collectors; + +public class ExactSplittingStrategy implements SplittingStrategy{ + List exactAmount; + + public ExactSplittingStrategy(List exactAmount) { + this.exactAmount = exactAmount; + } + + @Override + public List getSplit(Double amount) { + Double sum = 0.0; + for (Double i : exactAmount){ + sum += i; + } + + if(!sum.equals(amount)){ + throw new IllegalArgumentException("Sum of the amounts should be equal to total expense"); + } + + return exactAmount.stream() + .map(this::roundToTwoDecimalPlaces) + .collect(Collectors.toList()); + } + + private Double roundToTwoDecimalPlaces(Double value) { + return Math.round(value * 100.0) / 100.0; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/strategy/PercentageSplittingStrategy.java b/splitWise/src/main/java/org/machinecoding/strategy/PercentageSplittingStrategy.java new file mode 100644 index 00000000..d4ec0f1d --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/strategy/PercentageSplittingStrategy.java @@ -0,0 +1,32 @@ +package org.machinecoding.strategy; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class PercentageSplittingStrategy implements SplittingStrategy{ + List percentages; + + public PercentageSplittingStrategy(List percentages){ + this.percentages = percentages; + } + + @Override + public List getSplit(Double amount) { + Double sum = 0.0; + for (Double percentage : percentages) { + sum += percentage; + } + if(!sum.equals(100.0)){ + throw new IllegalArgumentException("Sum of percentage must be equal to 100"); + } + + return percentages.stream() + .map(t -> roundToTwoDecimalPlaces(t * amount / 100.00)) + .collect(Collectors.toList()); + } + + private Double roundToTwoDecimalPlaces(Double value) { + return Math.round(value * 100.0) / 100.0; + } +} diff --git a/splitWise/src/main/java/org/machinecoding/strategy/SplittingStrategy.java b/splitWise/src/main/java/org/machinecoding/strategy/SplittingStrategy.java new file mode 100644 index 00000000..28c5bb17 --- /dev/null +++ b/splitWise/src/main/java/org/machinecoding/strategy/SplittingStrategy.java @@ -0,0 +1,7 @@ +package org.machinecoding.strategy; + +import java.util.List; + +public interface SplittingStrategy { + List getSplit(Double amount); +}