Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d92271f
feat: ๊ถ์„ฑ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
boyekim Apr 6, 2026
f7ef7c6
feat: ๊ถ ํฌํš ์‹œ ์ข…๋ฃŒ ๋กœ์ง ์ถ”๊ฐ€
boyekim Apr 6, 2026
a94521d
feat: ๋ง ์„ ํƒ ์ทจ์†Œ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
boyekim Apr 6, 2026
87ef1ba
refactor: ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ ์ œ๊ฑฐ
boyekim Apr 6, 2026
8165335
feat: ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
boyekim Apr 7, 2026
65be702
test: ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
boyekim Apr 7, 2026
0bf8a46
build: build.gradle ์˜์กด์„ฑ ์ถ”๊ฐ€
boyekim Apr 8, 2026
e972bc8
feat: ์Šคํ‚ค๋งˆ ํŒŒ์ผ ์ถ”๊ฐ€
boyekim Apr 8, 2026
679ccd1
feat: ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค ์ƒ์„ฑ ๋กœ์ง ์ถ”๊ฐ€
boyekim Apr 8, 2026
973782d
feat: ๋ ˆํฌ์ง€ํ† ๋ฆฌ ์ฝ”๋“œ ์ถ”๊ฐ€
boyekim Apr 8, 2026
1d1b1f9
feat: DB ์ €์žฅ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
boyekim Apr 8, 2026
348f8c7
test: ๊ถ์„ฑ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
boyekim Apr 8, 2026
ef1561c
refactor: AppConfig ์ถ”๊ฐ€๋กœ ๊ฐ์ฒด ์ƒ์„ฑ ๋กœ์ง ๋ถ„๋ฆฌ
boyekim Apr 8, 2026
e2a542e
test: ๊ถ์„ฑ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
boyekim Apr 8, 2026
58c3ec8
test: ๊ฒŒ์ž„ ์ข…๋ฃŒ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
boyekim Apr 8, 2026
2416820
refactor: Piece์˜ Board ์˜์กด์„ฑ ์ œ๊ฑฐ
boyekim Apr 8, 2026
c2fc2fc
test: ํ…Œ์ŠคํŠธ ์ˆ˜์ •
boyekim Apr 8, 2026
85ba374
refactor: cancel ์ฒ˜๋ฆฌ ์ฑ…์ž„ InputView๋กœ ์ด๊ด€
boyekim Apr 12, 2026
e95429f
refactor: ๊ฒŒ์ž„ ์‹คํ–‰๊ณผ ์ €์žฅ ๊ฐ„๊ทน ๊ฐ์†Œ
boyekim Apr 12, 2026
d3c641a
test: Service ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€
boyekim Apr 12, 2026
b9063cf
feat: timeout ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€
boyekim Apr 13, 2026
5ea7a59
refactor: Winner ๋ฐ˜ํ™˜ ๋กœ์ง ์ˆ˜์ •
boyekim Apr 13, 2026
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
58 changes: 0 additions & 58 deletions .codex/skills/SKILL.md

This file was deleted.

3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ dependencies {
testImplementation platform('org.assertj:assertj-bom:3.27.3')
testImplementation('org.junit.jupiter:junit-jupiter')
testImplementation('org.assertj:assertj-core')
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.xerial:sqlite-jdbc:3.46.1.3'
}

java {
Expand Down
14 changes: 6 additions & 8 deletions src/main/java/janggi/Application.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package janggi;

import janggi.view.output.ConsoleOutputView;
import janggi.view.input.ConsoleInputView;
import janggi.view.input.InputView;
import janggi.view.output.OutputView;
import janggi.config.AppConfig;
import janggi.persistence.DatabaseInitializer;

public class Application {

public static void main(String[] args) {
InputView inputView = new ConsoleInputView();
OutputView outputView = new ConsoleOutputView();
JanggiRunner janggiRunner = new JanggiRunner(inputView, outputView);
janggiRunner.execute();
AppConfig appConfig = new AppConfig();
DatabaseInitializer databaseInitializer = appConfig.databaseInitializer();
databaseInitializer.initialize();
appConfig.janggiRunner().execute();
}
}
57 changes: 47 additions & 10 deletions src/main/java/janggi/JanggiRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,66 @@

import janggi.domain.JanggiGame;
import janggi.domain.Position;
import janggi.domain.WinnerResult;
import janggi.service.JanggiService;
import janggi.util.ActionExecutor;
import janggi.util.DelimiterParser;
import janggi.util.PersistenceExecutor;
import janggi.view.input.InputView;
import janggi.view.output.OutputView;
import java.util.List;
import java.util.Optional;

public class JanggiRunner {

private final InputView inputView;
private final OutputView outputView;
private final JanggiService janggiService;

public JanggiRunner(InputView inputView, OutputView outputView) {
public JanggiRunner(InputView inputView, OutputView outputView, JanggiService janggiService) {
this.inputView = inputView;
this.outputView = outputView;
this.janggiService = janggiService;
}

public void execute() {
outputView.printStartMessage();

JanggiGame janggiGame = JanggiGame.createInitialJanggiGame();
while (true) {
JanggiGame janggiGame = PersistenceExecutor.retryOnTimeout(
janggiService::loadGame,
this::printPersistenceTimeoutMessage
);
while (isGameContinue(janggiGame)) {
outputView.printBoard(janggiGame.makeCurrentTurnBoardSnapShot());
Position startPosition = ActionExecutor.retryUntilSuccess(
() -> readValidStartPosition(janggiGame), outputView
);
Position endPosition = ActionExecutor.retryUntilSuccess(
Optional<Position> endPosition = ActionExecutor.retryUntilSuccess(
() -> readValidEndPosition(janggiGame, startPosition), outputView
);
janggiGame.doGame(startPosition, endPosition);
if (endPosition.isEmpty()) {
outputView.printMessage("๋ง ์„ ํƒ์„ ์ทจ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์„ ํƒํ•ด์ฃผ์„ธ์š”.");
continue;
}
PersistenceExecutor.retryOnTimeout(
() -> janggiService.play(janggiGame, startPosition, endPosition.get()),
this::printPersistenceTimeoutMessage
);
Comment on lines +45 to +48
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

์‚ด์ง ์•„์‰ฌ์šธ ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์€ ์‚ฌ์‹ค Runner์˜ ์ž…์žฅ์—์„œ Timeout์— ๋Œ€ํ•œ retry๋กœ์ง์€ ๊ด€์‹ฌ์‚ฌ๋‚˜ ์ฑ…์ž„์ด ์•„๋‹ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜คํžˆ๋ ค Repository๋‚˜ DAO์˜ ์—ญํ• ์— ์ข€ ๋” ๊ฐ€๊นŒ์šด๋ฐ, print ๋•Œ๋ฌธ์— ์ด ๋ถ€๋ถ„์—์„œ ์ฒ˜๋ฆฌํ•˜๋ ค๊ณ  ํ•˜์‹  ๊ฒƒ ๊ฐ™์•„์š”. ํƒ€์ž„์•„์›ƒ๊ณผ ๋ฆฌํŠธ๋ผ์ด ๋ฉ”์„ธ์ง€๋ฅผ ๊ตณ์ด ์œ ์ €์—๊ฒŒ ๋ณด์—ฌ์ค„ ํ•„์š”๊ฐ€ ์žˆ์„๊นŒ์š”? ๋กœ๊ทธ๋กœ ๋‚จ๊ธฐ๋ฉด ์•ˆ๋˜๋‚˜์š”? ๋งŒ์•ฝ ๋ณด์—ฌ์ค˜์•ผํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ํ˜„์žฌ ๊ตฌ์กฐ๊ฐ€ ๋งž์„ ๊ฒƒ ๊ฐ™๊ธด ํ•ฉ๋‹ˆ๋‹ค๋งŒ ํ™•์‹คํžˆ ์ข€ ์ฝ”๋“œ๊ฐ€ ์ฝ์ด ์–ด๋ ค์›Œ์ง€๋Š” ๋ถ€๋ถ„์€ ์žˆ์Šต๋‹ˆ๋‹ค!

}
finish(janggiGame);
}

private void finish(JanggiGame janggiGame) {
PersistenceExecutor.retryOnTimeout(janggiService::finishGame, this::printPersistenceTimeoutMessage);
WinnerResult winnerResult = janggiGame.getWinnerResult();
outputView.printResult(
janggiGame.makeCurrentTurnBoardSnapShot(),
winnerResult.winner(),
winnerResult.winnerScore()
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

๊ตฌ์›ƒ ํ”ผ๋“œ๋ฐฑ ์ž˜ ๋ฐ˜์˜ํ•ด์ฃผ์…จ๋„ค์š”!

}

private boolean isGameContinue(JanggiGame janggiGame) {
return !janggiGame.isGameOver();
}

private Position readValidStartPosition(JanggiGame janggiGame) {
Expand All @@ -44,12 +74,19 @@ private Position readValidStartPosition(JanggiGame janggiGame) {
return startPosition;
}

private Position readValidEndPosition(JanggiGame janggiGame, Position startPosition) {
private Optional<Position> readValidEndPosition(JanggiGame janggiGame, Position startPosition) {
outputView.printAskMovePosition(janggiGame.findPiece(startPosition).nickname());
String rawMovePosition = inputView.readLine();
List<String> parsedMovePosition = DelimiterParser.parse(rawMovePosition);
Optional<String> rawMovePosition = inputView.readCancelableLine();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Empty(None) -> cancel, String -> move ์ด๋Ÿฐ์‹์œผ๋กœ ๊ตฌ๋ถ„์„ ํ•˜์…จ๊ตฐ์š”? ๊ดœ์ฐฎ๊ธด ํ•œ๋ฐ, ์ด๊ฒŒ ํ˜ธ์ถœ์ž ์ž…์žฅ์—์„œ๋Š” Optional์ด ๊ธฐ์กด ์˜๋ฏธ๋ž‘ ๋‹ค๋ฅด๊ฒŒ ์‚ฌ์šฉ๋ฌ๋‹ค๋Š”๊ฑธ ์•Œ์•„์•ผํ•˜๊ณ , ์ด ๋ถ€๋ถ„ ์˜๋ฏธ๋ฅผ ์•Œ๊ธฐ ์œ„ํ•ด ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ๋ฐฉ์‹์„ ์•Œ์•„์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€ ๋ณด์ˆ˜ํ•˜๊ธฐ ์ข‹์€ ์ฝ”๋“œ๋Š” ์•„๋‹Œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ์ œ ์ƒ๊ฐ์—๋Š” ์ฝ”๋“œ๋ฅผ ์ฝ์—ˆ์„๋•Œ ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฐ๊ฒฝ ์ง€์‹์ด ํ•„์š” ์—†๊ณ , ๊ตฌํ˜„์ฒด๋ฅผ ๋ณผ ํ•„์š”๊ฐ€ ์—†๋Š” ์ฝ”๋“œ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋œ ์ฝ”๋“œ๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ฑฐ๋“ ์š”. ๋ฌผ๋ก  ๋‹ค๋ฅด๊ฒŒ ์ƒ๊ฐํ•˜์‹ค ์ˆ˜ ์žˆ์ง€๋งŒ ์ด๋Ÿฐ ๊ธฐ์ค€๋„ ์žˆ๋‹ค ๋ง์”€๋“œ๋ฆฌ๋Š” ๊ฒ๋‹ˆ๋‹ค!

if (rawMovePosition.isEmpty()) {
return Optional.empty();
}
List<String> parsedMovePosition = DelimiterParser.parse(rawMovePosition.get());
Position endPosition = Position.makePosition(parsedMovePosition);
janggiGame.validateValidEndPosition(startPosition, endPosition);
return endPosition;
return Optional.of(endPosition);
}

private void printPersistenceTimeoutMessage() {
outputView.printMessage("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‘๋‹ต์ด ์ง€์—ฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.");
}
}
38 changes: 38 additions & 0 deletions src/main/java/janggi/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package janggi.config;

import janggi.JanggiRunner;
import janggi.persistence.DatabaseInitializer;
import janggi.persistence.repository.JanggiGameRepository;
import janggi.persistence.repository.JdbcJanggiGameRepository;
import janggi.service.JanggiService;
import janggi.view.input.ConsoleInputView;
import janggi.view.input.InputView;
import janggi.view.output.ConsoleOutputView;
import janggi.view.output.OutputView;

public class AppConfig {

public DatabaseInitializer databaseInitializer() {
return new DatabaseInitializer();
}

public JanggiRunner janggiRunner() {
return new JanggiRunner(inputView(), outputView(), janggiService());
}

public JanggiService janggiService() {
return new JanggiService(janggiGameRepository());
}

public JanggiGameRepository janggiGameRepository() {
return new JdbcJanggiGameRepository();
}

public InputView inputView() {
return new ConsoleInputView();
}

public OutputView outputView() {
return new ConsoleOutputView();
}
}
76 changes: 67 additions & 9 deletions src/main/java/janggi/domain/Board.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package janggi.domain;

import janggi.domain.movepath.MovePathStrategy;
import janggi.domain.piece.Piece;
import janggi.domain.piece.PieceType;
import janggi.domain.team.Team;
import janggi.domain.team.TeamType;
import janggi.dto.BoardSpot;
import janggi.dto.BoardSpots;
import janggi.dto.MoveRoute;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

Expand Down Expand Up @@ -55,11 +59,7 @@ public Optional<Piece> findPiece(Position position) {
.findFirst();
}

public boolean hasPiece(Position position) {
return findPiece(position).isPresent();
}

public void canMove(Position startPosition, Position endPosition, TeamType playingTeam) {
public void validateMove(Position startPosition, Position endPosition, TeamType playingTeam) {
validateRange(startPosition);
validateRange(endPosition);
Piece piece = findTeamPiece(startPosition, currentTeam(playingTeam));
Expand All @@ -72,12 +72,33 @@ public Board move(
Position endPosition,
TeamType playingTeam
) {
canMove(startPosition, endPosition, playingTeam);
validateMove(startPosition, endPosition, playingTeam);
Team movedCurrentTeam = currentTeam(playingTeam).move(startPosition, endPosition);
Team remainedOpponentTeam = removeOpponentPiece(playingTeam, endPosition);
return createMovedBoard(playingTeam, movedCurrentTeam, remainedOpponentTeam);
}

public boolean hasGung(TeamType teamType) {
return findSpecificTeam(teamType).hasPieceType(PieceType.GUNG);
}

public Optional<TeamType> findWinner() {
boolean chuHasGung = hasGung(TeamType.CHU);
boolean hanHasGung = hasGung(TeamType.HAN);
if (chuHasGung == hanHasGung) {
return Optional.empty();
}
if (chuHasGung) {
return Optional.of(TeamType.CHU);
}
return Optional.of(TeamType.HAN);
}

public int calculateScore(TeamType teamType) {
Team team = findSpecificTeam(teamType);
return team.calculatePiecesScore();
}

private Piece findTeamPiece(Position position, Team nowTeam) {
return nowTeam.findPiece(position)
.orElseThrow(() -> new IllegalArgumentException("์ž…๋ ฅํ•œ ์œ„์น˜์— ๊ธฐ๋ฌผ์ด ์—†์Šต๋‹ˆ๋‹ค."));
Expand All @@ -92,15 +113,52 @@ private Team opponentTeam(TeamType nowTurnTeamType) {
}

private void validateCanMove(Piece piece, Position piecePosition, Position targetPosition) {
if (!piece.isValidMovePattern(piecePosition.getX(), piecePosition.getY(), targetPosition.getX(),
targetPosition.getY())) {
Optional<MovePathStrategy> movePath = piece.findMovePath(
piecePosition.getX(),
piecePosition.getY(),
targetPosition.getX(),
targetPosition.getY()
);
if (movePath.isEmpty()) {
throw new IllegalArgumentException("์ด๋™ํ•  ์ˆ˜ ์—†๋Š” ์œ„์น˜์ž…๋‹ˆ๋‹ค.");
}
if (!piece.isObstaclesNotExist(piecePosition, targetPosition, this)) {
MoveRoute moveRoute = createMoveRoute(piecePosition, targetPosition, movePath.get());
if (!piece.canMove(moveRoute)) {
throw new IllegalArgumentException("์ด๋™ํ•  ์ˆ˜ ์—†๋Š” ์œ„์น˜์ž…๋‹ˆ๋‹ค.");
}
}

private MoveRoute createMoveRoute(
Position startPosition,
Position targetPosition,
MovePathStrategy movePath
) {
return new MoveRoute(
findIntermediatePieceTypes(startPosition, targetPosition, movePath),
findTargetPieceType(targetPosition)
);
}

private List<PieceType> findIntermediatePieceTypes(
Position startPosition,
Position targetPosition,
MovePathStrategy movePath
) {
return movePath.intermediatePositions(startPosition, targetPosition).stream()
.map(this::findPieceType)
.flatMap(Optional::stream)
.toList();
}

private Optional<PieceType> findTargetPieceType(Position targetPosition) {
return findPieceType(targetPosition);
}

private Optional<PieceType> findPieceType(Position position) {
return findPiece(position)
.map(Piece::getPieceType);
}

private void validateTargetPosition(Team team, Position targetPosition) {
if (team.findPiece(targetPosition).isPresent()) {
throw new IllegalArgumentException("๊ฐ™์€ ํŒ€์˜ ๊ธฐ๋ฌผ์ด ์žˆ๋Š” ์œ„์น˜๋กœ๋Š” ์ด๋™ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
Expand Down
Loading