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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 4단계 - 게임 실행

## 기능 요구사항
- 사다리 게임에 참여하는 사람에 이름을 최대 5글자까지 부여할 수 있다. 사다리를 출력할 때 사람 이름도 같이 출력한다.
- 사람 이름은 쉼표(,)를 기준으로 구분한다.
- 개인별 이름을 입력하면 개인별 결과를 출력하고, "all"을 입력하면 전체 참여자의 실행 결과를 출력한다.

```text
참여할 사람 이름을 입력하세요. (이름은 쉼표(,)로 구분하세요)
neo,brown,brie,tommy

실행 결과를 입력하세요. (결과는 쉼표(,)로 구분하세요)
꽝,5000,꽝,3000

최대 사다리 높이는 몇 개인가요?
5

사다리 결과

neo brown brie tommy
|-----| |-----|
| |-----| |
|-----| | |
| |-----| |
|-----| |-----|
꽝 5000 꽝 3000

결과를 보고 싶은 사람은?
neo

실행 결과

결과를 보고 싶은 사람은?
all

실행 결과
neo : 꽝
brown : 3000
brie : 꽝
tommy : 5000

```

# 3단계 - 사다리 타기

## 기능 요구사항
- 사다리의 시작 지점과 도착 지점을 출력한다.

```text
사다리의 넓이는 몇 개인가요?
4

사다리의 높이는 몇 개인가요?
5

실행결과

|-----| |-----|
| |-----| |
|-----| | |
| |-----| |
|-----| |-----|

0 -> 0
1 -> 3
2 -> 2
3 -> 1
```

# 2단계 - 사다리 생성

## 기능 요구사항
- 사다리는 크기를 입력 받아 생성할 수 있다.

```text
사다리의 넓이는 몇 개인가요?
4

사다리의 높이는 몇 개인가요?
5

실행결과

|-----| |-----|
| |-----| |
|-----| | |
| |-----| |
|-----| |-----|
```

# 1단계 - 사다리 출력

## 기능 요구사항

- 네이버 사다리 게임을 참고하여 도메인을 분석하여 구현한다.
- 사다리는 4X4 크기로 고정되고, 연결 여부는 랜덤으로 결정한다.
- 사다리 타기가 정상적으로 동작하려면 라인이 겹치지 않도록 해야 한다.

### 실행 결과
- 프로그램을 실행한 결과는 다음과 같다.

```text
실행결과

|-----| |-----|
| |-----| |
|-----| | |
| |-----| |
```

## 추가된 요구 사항
- 모든 엔티티를 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
Empty file removed src/main/java/.gitkeep
Empty file.
12 changes: 12 additions & 0 deletions src/main/java/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import controller.LadderController;
import view.InputView;
import view.OutputView;

public class Application {
public static void main(String[] args) {
InputView inputView = new InputView();
OutputView outputView = new OutputView();
LadderController controller = new LadderController(inputView, outputView);
controller.run();
}
}
89 changes: 89 additions & 0 deletions src/main/java/controller/LadderController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package controller;

import domain.LadderBoard;
import domain.LadderPath;
import domain.Participants;
import domain.PlayerResults;
import dto.LadderBuildResponse;
import dto.LadderResultResponse;
import view.InputView;
import view.OutputView;

import java.util.List;

public class LadderController {

private static final String ALL_COMMAND = "all";

private final InputView inputView;
private final OutputView outputView;

public LadderController(InputView inputView, OutputView outputView) {
this.inputView = inputView;
this.outputView = outputView;
}

public void run() {
Participants participants = readParticipants();
List<String> resultLabels = readResultLabels();
int ladderHeight = readLadderHeight();

LadderBuildResponse ladder = buildLadder(participants.getCount(), ladderHeight);
printLadderBoard(participants.getNames(), resultLabels, ladder);

PlayerResults playerResults = mapPlayerResults(participants, resultLabels, ladder);
handleResultRequest(playerResults);
}

private Participants readParticipants() {
outputView.printParticipantPrompt();
List<String> names = inputView.readParticipantNames();
return Participants.of(names);
}

private List<String> readResultLabels() {
outputView.printResultPrompt();
return inputView.readResultLabels();
}

private int readLadderHeight() {
outputView.printHeightPrompt();
return inputView.readLadderHeight();
}

private LadderBuildResponse buildLadder(int columnCount, int rowCount) {
LadderBoard ladderBoard = LadderBoard.build(columnCount, rowCount);
return LadderBuildResponse.from(ladderBoard);
}

private void printLadderBoard(List<String> participants, List<String> results, LadderBuildResponse ladder) {
outputView.printLadderTitle();
outputView.printParticipantNames(participants);
outputView.printBridgeLines(ladder);
outputView.printResultLabels(results);
}

private PlayerResults mapPlayerResults(Participants participants, List<String> results, LadderBuildResponse ladder) {
LadderPath ladderPath = new LadderPath(ladder.lines(), ladder.columnCount());
LadderResultResponse resultMapping = new LadderResultResponse(ladderPath.mapStartToEndIndex());
return PlayerResults.from(participants, results, resultMapping.positionMap());
}

private void handleResultRequest(PlayerResults playerResults) {
outputView.printResultSelectionPrompt();
String name = inputView.readResultRequest();

if (ALL_COMMAND.equals(name)) {
outputView.printLadderTitle();
outputView.printAllResults(playerResults.allResults());
return;
}

if (playerResults.hasPlayer(name)) {
outputView.printSingleResultWithTitle(playerResults.resultOf(name));
return;
}

outputView.printNameNotFound();
}
}
45 changes: 45 additions & 0 deletions src/main/java/domain/BridgeLine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package domain;

import java.util.Collections;
import java.util.List;

public class BridgeLine {

private static final String ERROR_INVALID_CONNECTIONS = "[ERROR] 가로줄 연결 상태는 null이거나 비어 있을 수 없습니다.";

private final List<Boolean> horizontalConnections;

public BridgeLine(List<Boolean> horizontalConnections) {
validate(horizontalConnections);
this.horizontalConnections = Collections.unmodifiableList(horizontalConnections);
}

public boolean isConnectedAt(int index) {
if (index < 0 || index >= horizontalConnections.size()) {
return false;
}
return horizontalConnections.get(index);
}

public int width() {
return horizontalConnections.size();
}

public int nextPositionFrom(int position) {
if (position > 0 && isConnectedAt(position - 1)) {
return position - 1;
}

if (position < width() && isConnectedAt(position)) {
return position + 1;
}

return position;
}

private void validate(List<Boolean> connections) {
if (connections == null || connections.isEmpty()) {
throw new IllegalArgumentException(ERROR_INVALID_CONNECTIONS);
}
}
}
108 changes: 108 additions & 0 deletions src/main/java/domain/LadderBoard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package domain;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class LadderBoard {

private static final Random RANDOM = new Random();
private static final int MAX_ATTEMPTS = 100;
private static final int MIN_PARTICIPANTS = 2;
private static final int MIN_HEIGHT = 1;

private static final String ERROR_TOO_FEW_PARTICIPANTS = "[ERROR] 사다리는 최소 2명 이상의 참가자가 필요합니다.";
private static final String ERROR_INVALID_HEIGHT = "[ERROR] 사다리 높이는 1 이상이어야 합니다.";
private static final String ERROR_GENERATION_FAILED = "[ERROR] 사다리를 생성하지 못했습니다. 참가자 수나 높이를 확인해 주세요.";

private final int columnCount;
private final List<BridgeLine> lines;

private LadderBoard(int columnCount, List<BridgeLine> lines) {
this.columnCount = columnCount;
this.lines = lines;
}

public static LadderBoard build(int columnCount, int rowCount) {
validate(columnCount, rowCount);
int centerColumnIndex = (columnCount - 1) / 2;

List<BridgeLine> bridgeLines = tryGenerateBridgeLines(columnCount, rowCount, centerColumnIndex);
return new LadderBoard(columnCount, bridgeLines);
}

public int getColumnCount() {
return columnCount;
}

public List<BridgeLine> getLines() {
return lines;
}

private static void validate(int columnCount, int rowCount) {
if (columnCount < MIN_PARTICIPANTS) {
throw new IllegalArgumentException(ERROR_TOO_FEW_PARTICIPANTS);
}
if (rowCount < MIN_HEIGHT) {
throw new IllegalArgumentException(ERROR_INVALID_HEIGHT);
}
}

private static List<BridgeLine> tryGenerateBridgeLines(int participantCount, int ladderHeight, int centerColumnIndex) {
return IntStream.range(0, MAX_ATTEMPTS)
.mapToObj(i -> generateAttempt(participantCount, ladderHeight))
.filter(lines -> isValidBridgeLines(lines, participantCount, centerColumnIndex))
.findFirst()
.orElseThrow(() -> new IllegalStateException(ERROR_GENERATION_FAILED));
}

private static List<BridgeLine> generateAttempt(int participantCount, int ladderHeight) {
return IntStream.range(0, ladderHeight)
.mapToObj(i -> new BridgeLine(generateConnectionStates(participantCount)))
.collect(Collectors.toList());
}

private static boolean isValidBridgeLines(List<BridgeLine> lines, int participantCount, int centerColumnIndex) {
return hasCenterBridge(lines, centerColumnIndex) &&
hasAllColumnConnections(lines, participantCount - 1);
}

private static boolean hasCenterBridge(List<BridgeLine> lines, int centerIndex) {
return lines.stream().anyMatch(line -> isConnectedAtCenter(line, centerIndex));
}

private static boolean hasAllColumnConnections(List<BridgeLine> lines, int expectedCount) {
Set<Integer> connectedIndices = new HashSet<>();
lines.forEach(line -> recordConnectedIndices(connectedIndices, getConnectionStates(line)));
return connectedIndices.size() == expectedCount;
}

private static List<Boolean> getConnectionStates(BridgeLine line) {
return IntStream.range(0, line.width())
.mapToObj(line::isConnectedAt)
.collect(Collectors.toList());
}

private static List<Boolean> generateConnectionStates(int participantCount) {
return IntStream.range(0, participantCount - 1)
.boxed()
.collect(ArrayList::new, (list, i) -> {
boolean canConnect = i == 0 || !list.get(i - 1);
list.add(canConnect && RANDOM.nextBoolean());
}, List::addAll);
}

private static boolean isConnectedAtCenter(BridgeLine line, int centerIndex) {
return centerIndex < line.width() && line.isConnectedAt(centerIndex);
}
Comment on lines +99 to +101
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그런데 isConnectedAtCenter는 어떤 의도로 필요한가요? 프로그램 요구사항 중에 중앙 사다리에 반드시 연결되어 있어야 한다는 못본 것 같아서요!

Copy link
Author

@jsoonworld jsoonworld May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아, 이 부분은 구현하면서 중간에 추가한 로직인데요!

중간에 사다리가 너무 한쪽으로만 쏠리거나, 중앙이 전혀 연결되지 않아서
사다리 전체가 갈 길을 잃은 듯한 구조가 만들어지는 걸 실제로 여러 번 봤거든요 😅
그래서 "최소한 중앙은 한 번쯤 연결되어 있어야 흐름이 생기겠다"는 생각으로
isConnectedAtCenter 조건을 넣게 되었습니다.

물론 말씀해주신 것처럼, 요구사항 상에 ‘중앙 연결이 필수’라는 조건은 명시되어 있지 않아요.
그럼에도 이 조건을 추가한 이유는, 랜덤으로 생성되는 특성상
흐름이 단절되거나 사다리가 지나치게 한쪽으로 치우치는 걸 방지하고 싶었기 때문이에요.

다만 지금 다시 보니, 요구사항에 없는 조건을 도메인 로직처럼 넣어버린 게
다이어트를 하다가 정크푸드(객체지향)를 먹은 느낌이네요! 😅 개선하는 게 좋을까요!?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그런 의도에서였군요. 그런 의도라면 굳이 제거할 필요는 없을 것 같습니다! 다만 제가 테스트해보지는 않은건데, 정말 많은 수의 인원이 참여하는 게임에서도 잘 연결되는지 체크해보면 더 좋을 것 같아요!


private static void recordConnectedIndices(Set<Integer> connectedColumnIndices, List<Boolean> connectionStates) {
IntStream.range(0, connectionStates.size())
.filter(i -> connectionStates.get(i))
.forEach(connectedColumnIndices::add);
}
}
Loading