diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..41879b2c --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,14 @@ +import controller.RaceController; +import view.RaceOutputView; + +import java.util.Random; + +public class Application { + + public static void main(String[] args) { + Random random = new Random(); + RaceOutputView raceOutputView = new RaceOutputView(); + RaceController raceController = new RaceController(raceOutputView, random); + raceController.start(); + } +} diff --git a/src/main/java/controller/RaceController.java b/src/main/java/controller/RaceController.java new file mode 100644 index 00000000..3a2d3c61 --- /dev/null +++ b/src/main/java/controller/RaceController.java @@ -0,0 +1,34 @@ +package controller; + +import model.Car; +import model.Racing; +import model.dto.RacingPlayResponse; +import view.RaceOutputView; +import view.UserInputView; + +import java.util.Random; + +public class RaceController { + + private final RaceOutputView raceOutputView; + private final Random random; + + public RaceController(RaceOutputView raceOutputView, Random random) { + this.raceOutputView = raceOutputView; + this.random = random; + } + + public void start() { + raceOutputView.inputCarNames(); + String carNames = UserInputView.readStringInput(); + raceOutputView.inputRaceCount(); + int raceCount = UserInputView.readIntegerInput(); + + Racing racing = new Racing(random); + RacingPlayResponse response = racing.play(carNames, raceCount); + + raceOutputView.printRacingData(response.moveData()); + raceOutputView.printWinners(response.winners() + .stream().map(Car::getName).toList()); + } +} diff --git a/src/main/java/model/Car.java b/src/main/java/model/Car.java new file mode 100644 index 00000000..f012d126 --- /dev/null +++ b/src/main/java/model/Car.java @@ -0,0 +1,41 @@ +package model; + +import java.util.Random; + +public class Car { + + private final Random random; + private final String name; + private int position = 1; + + + public Car(String name, Random random) { + this.name = name; + validateCarName(name); + this.random = random; + } + + private void validateCarName(String name) { + if(name == null || name.isBlank() || name.length() > 5) { + throw new IllegalArgumentException("5자 이하의 이름을 작성해주세요."); + } + } + + public void move() { + if (canMove()) { + position++; + } + } + + private boolean canMove() { + return random.nextInt(10) >= 4; + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/model/Cars.java b/src/main/java/model/Cars.java new file mode 100644 index 00000000..96cf2ebf --- /dev/null +++ b/src/main/java/model/Cars.java @@ -0,0 +1,31 @@ +package model; + +import java.util.List; +import java.util.Random; + +public class Cars { + + private final List cars; + + public Cars(Random random, List carNames) { + this.cars = carNames.stream() + .map(carName -> new Car(carName, random)) + .toList(); + + } + + public void allMove(){ + cars.forEach(Car::move); + } + + public int findMaxDistance(){ + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(0); + } + + public List getCars() { + return cars; + } +} diff --git a/src/main/java/model/Racing.java b/src/main/java/model/Racing.java new file mode 100644 index 00000000..ba3fc228 --- /dev/null +++ b/src/main/java/model/Racing.java @@ -0,0 +1,52 @@ +package model; + +import model.dto.RacingPlayResponse; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class Racing { + + private final Random rand; + + public Racing(Random rand) { + this.rand = rand; + } + + public RacingPlayResponse play(String inputCarNames, int raceCount) { + List carNames = Arrays.stream(inputCarNames.split(",")).toList(); + Cars cars = new Cars(rand, carNames); + List> moveData = runRacing(raceCount, cars); + + return new RacingPlayResponse(moveData, getWinners(cars)); + } + + private List> runRacing(int raceCount, Cars cars) { + List> moveData = new ArrayList<>(); + moveData.add(getSnapshot(cars)); + IntStream.range(0, raceCount) + .forEach(i -> moveData.add(moveAndGetSnapshot(cars))); + + return moveData; + } + + private Map moveAndGetSnapshot(Cars cars) { + cars.allMove(); + + return getSnapshot(cars); + } + + private Map getSnapshot(Cars cars) { + return cars.getCars().stream() + .collect(Collectors.toMap(Car::getName, Car::getPosition)); + } + + private List getWinners(Cars cars) { + int maxDistance = cars.findMaxDistance(); + + return cars.getCars().stream() + .filter(car -> car.getPosition() == maxDistance) + .toList(); + } +} diff --git a/src/main/java/model/dto/RacingPlayResponse.java b/src/main/java/model/dto/RacingPlayResponse.java new file mode 100644 index 00000000..68c04d83 --- /dev/null +++ b/src/main/java/model/dto/RacingPlayResponse.java @@ -0,0 +1,12 @@ +package model.dto; + +import model.Car; + +import java.util.List; +import java.util.Map; + +public record RacingPlayResponse( + List> moveData, + List winners +) { +} diff --git a/src/main/java/view/RaceOutputView.java b/src/main/java/view/RaceOutputView.java new file mode 100644 index 00000000..8257eee8 --- /dev/null +++ b/src/main/java/view/RaceOutputView.java @@ -0,0 +1,30 @@ +package view; + +import java.util.List; +import java.util.Map; + +public class RaceOutputView { + + public void inputCarNames() { + System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + } + + public void inputRaceCount() { + System.out.println("시도할 회수는 몇회인가요?"); + } + + public void printRacingData(List> moveData) { + System.out.println("\n실행 결과"); + for (Map data : moveData) { + data.forEach( + (key, value) -> + System.out.println(key + " : " + "-".repeat(value)) + ); + System.out.println(); + } + } + + public void printWinners(List winnerNames) { + System.out.println(String.join(", ", winnerNames) + "가 최종 우승했습니다."); + } +} diff --git a/src/main/java/view/UserInputView.java b/src/main/java/view/UserInputView.java new file mode 100644 index 00000000..af517ad4 --- /dev/null +++ b/src/main/java/view/UserInputView.java @@ -0,0 +1,16 @@ +package view; + +import java.util.Scanner; + +public class UserInputView { + + private static final Scanner scanner = new Scanner(System.in); + + public static String readStringInput() { + return scanner.nextLine(); + } + + public static int readIntegerInput() { + return scanner.nextInt(); + } +} diff --git a/src/test/java/common/FakeRandom.java b/src/test/java/common/FakeRandom.java new file mode 100644 index 00000000..2f632119 --- /dev/null +++ b/src/test/java/common/FakeRandom.java @@ -0,0 +1,23 @@ +package common; + +import java.util.List; +import java.util.Random; + +public class FakeRandom extends Random { + + private int index; + private final List fixedValues; + + public FakeRandom(List fixedValues) { + this.fixedValues = fixedValues; + } + + @Override + public int nextInt(int dummyBound) { + if(index >= fixedValues.size()) { + throw new IndexOutOfBoundsException(); + } + + return fixedValues.get(index++); + } +} diff --git a/src/test/java/controller/RaceControllerTest.java b/src/test/java/controller/RaceControllerTest.java new file mode 100644 index 00000000..7e315298 --- /dev/null +++ b/src/test/java/controller/RaceControllerTest.java @@ -0,0 +1,52 @@ +package controller; + +import common.FakeRandom; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import view.RaceOutputView; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; + +import static fixture.FakeNumber.winnersNumbers; +import static org.assertj.core.api.Assertions.assertThat; + +public class RaceControllerTest { + + private final RaceOutputView outputView = new RaceOutputView(); + + @Test + @DisplayName("OK : 자동차 레이싱 플레이") + void testRace_winners() throws IOException { + RaceController raceController = new RaceController(outputView, new FakeRandom(winnersNumbers)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream("neo,brie,brown\n5\n".getBytes()); + System.setIn(byteArrayInputStream); + + raceController.start(); + + String output = outputStream.toString(); + assertThat(output).contains("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + assertThat(output).contains("시도할 회수는 몇회인가요?"); + assertThat(output).contains("실행 결과"); + assertThat(output).contains("neo : -"); + assertThat(output).contains("neo : --"); + assertThat(output).contains("neo : ---"); + assertThat(output).contains("neo : ----"); + assertThat(output).contains("neo : -----"); + assertThat(output).contains("brie : -"); + assertThat(output).contains("brie : --"); + assertThat(output).contains("brie : ---"); + assertThat(output).contains("brie : ----"); + assertThat(output).contains("brown : -"); + assertThat(output).contains("brown : --"); + assertThat(output).contains("brown : ---"); + assertThat(output).contains("brown : ----"); + assertThat(output).contains("brown : -----"); + assertThat(output).contains("neo, brown가 최종 우승했습니다."); + byteArrayInputStream.close(); + } +} diff --git a/src/test/java/fixture/FakeNumber.java b/src/test/java/fixture/FakeNumber.java new file mode 100644 index 00000000..a974581a --- /dev/null +++ b/src/test/java/fixture/FakeNumber.java @@ -0,0 +1,23 @@ +package fixture; + +import java.util.List; + +public class FakeNumber { + + public static final int MOVE_NUMBER = 4; + public static final int STOP_NUMBER = 3; + public static final List winnerNumbers = List.of( + MOVE_NUMBER, MOVE_NUMBER, STOP_NUMBER, + STOP_NUMBER, MOVE_NUMBER, STOP_NUMBER, + STOP_NUMBER, MOVE_NUMBER, STOP_NUMBER, + STOP_NUMBER, MOVE_NUMBER, STOP_NUMBER, + STOP_NUMBER, MOVE_NUMBER, STOP_NUMBER + ); + public static final List winnersNumbers = List.of( + MOVE_NUMBER, STOP_NUMBER, MOVE_NUMBER, + MOVE_NUMBER, MOVE_NUMBER, MOVE_NUMBER, + MOVE_NUMBER, MOVE_NUMBER, MOVE_NUMBER, + MOVE_NUMBER, MOVE_NUMBER, MOVE_NUMBER, + STOP_NUMBER, STOP_NUMBER, STOP_NUMBER + ); +} diff --git a/src/test/java/model/CarTest.java b/src/test/java/model/CarTest.java new file mode 100644 index 00000000..e6d260c2 --- /dev/null +++ b/src/test/java/model/CarTest.java @@ -0,0 +1,50 @@ +package model; + +import common.FakeRandom; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; +import java.util.Random; + +import static fixture.FakeNumber.MOVE_NUMBER; +import static fixture.FakeNumber.STOP_NUMBER; +import static org.assertj.core.api.Assertions.assertThat; + +public class CarTest { + + @ParameterizedTest() + @DisplayName("OK : 랜덤값이 4 이상일 시 움직임") + @ValueSource(ints = {MOVE_NUMBER, 9}) + void carMove(int fixedValue){ + Car car = createCar(fixedValue); + car.move(); + + assertThat(car.getPosition()).isEqualTo(2); + } + + @ParameterizedTest() + @DisplayName("OK : 랜덤값이 4 미만일 시 움직이지 않음") + @ValueSource(ints = {0, STOP_NUMBER}) + void carNotMove(int fixedValue){ + Car car = createCar(fixedValue); + car.move(); + + assertThat(car.getPosition()).isEqualTo(1); + } + + @Test + @DisplayName("ERROR : 이름 형식이 잘못됨") + void InvalidNameCar(){ + Assertions.assertThrows(IllegalArgumentException.class, () -> new Car("", new Random())); + Assertions.assertThrows(IllegalArgumentException.class, () -> new Car(" ", new Random())); + Assertions.assertThrows(IllegalArgumentException.class, () -> new Car("test12", new Random())); + } + + private Car createCar(int fixedValue){ + return new Car("test1", new FakeRandom(List.of(fixedValue))); + } +} diff --git a/src/test/java/model/CarsTest.java b/src/test/java/model/CarsTest.java new file mode 100644 index 00000000..dcbb00a8 --- /dev/null +++ b/src/test/java/model/CarsTest.java @@ -0,0 +1,54 @@ +package model; + +import common.FakeRandom; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.IntStream; + +import static fixture.FakeNumber.MOVE_NUMBER; +import static fixture.FakeNumber.STOP_NUMBER; +import static org.assertj.core.api.Assertions.assertThat; + +public class CarsTest { + + private final List carsNames = List.of("neo", "brie", "brown"); + + @Test + @DisplayName("OK : 자동차들을 조회함") + void carsGetCars() { + Cars cars = createCars(); + List carGroup = cars.getCars(); + + assertThat(carGroup.size()).isEqualTo(3); + IntStream.range(0, carGroup.size()) + .forEach(i -> assertThat(carGroup.get(i).getName()) + .isEqualTo(carsNames.get(i))); + } + + @Test + @DisplayName("OK : 자동차들을 모두 움직임") + void carsAllMove() { + List expectPositionResult = List.of(2, 2, 1); + Cars cars = createCars(); + cars.allMove(); + + IntStream.range(0, cars.getCars().size()) + .forEach(i -> assertThat(cars.getCars().get(i).getPosition()) + .isEqualTo(expectPositionResult.get(i))); + } + + @Test + @DisplayName("OK : 자동차들 가장 멀리간 위치를 조회함") + void carsFindMaxDistance() { + Cars cars = createCars(); + cars.allMove(); + + assertThat(cars.findMaxDistance()).isEqualTo(2); + } + + private Cars createCars() { + return new Cars(new FakeRandom(List.of(MOVE_NUMBER, MOVE_NUMBER, STOP_NUMBER)), carsNames); + } +} diff --git a/src/test/java/model/RacingTest.java b/src/test/java/model/RacingTest.java new file mode 100644 index 00000000..c337a17c --- /dev/null +++ b/src/test/java/model/RacingTest.java @@ -0,0 +1,40 @@ +package model; + +import common.FakeRandom; +import model.dto.RacingPlayResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static fixture.FakeNumber.winnerNumbers; +import static fixture.FakeNumber.winnersNumbers; +import static org.assertj.core.api.Assertions.assertThat; + +public class RacingTest { + + @Test + @DisplayName("OK : 한 대가 우승함") + void playRacing_one_winners() { + Racing racing = new Racing(new FakeRandom(winnerNumbers)); + RacingPlayResponse response = racing.play("neo,brie,brown", 5); + assertThat(response.winners().size()).isEqualTo(1); + assertThat(response.winners().stream().map(Car::getName).toList()).contains("brie"); + } + + @Test + @DisplayName("OK : 다수가 우승함") + void playRacing_two_winners() { + Racing racing = new Racing(new FakeRandom(winnersNumbers)); + RacingPlayResponse response = racing.play("neo,brie,brown", 5); + assertThat(response.winners().size()).isEqualTo(2); + assertThat(response.winners().stream().map(Car::getName).toList()).contains("neo", "brown"); + } + + @Test + @DisplayName("OK : raceCount가 0이면 모두 공동 우승") + void playRacing_zero_raceCount() { + Racing racing = new Racing(new FakeRandom(winnerNumbers)); + RacingPlayResponse response = racing.play("neo,brie,brown", 0); + assertThat(response.winners().size()).isEqualTo(3); + assertThat(response.winners().stream().map(Car::getName).toList()).contains("neo","brie","brown"); + } +}