Skip to content

Commit

Permalink
세션을 검증할 수 있다 (#170)
Browse files Browse the repository at this point in the history
* feat: 세션 검증V2 구현

* refactor: 코틀린 마이그레이션

* feat(snackgame): 테스트 작성

* refactor: 리뷰 반영

* feat(board): 테스트 추가

* fix: AttributeConverter 분리

* fix: 테스트 수정
  • Loading branch information
Hwanvely authored Sep 2, 2024
1 parent c2d9b1b commit 54612a9
Show file tree
Hide file tree
Showing 46 changed files with 865 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.time.LocalDateTime;
import java.util.List;

import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
Expand Down Expand Up @@ -37,46 +38,47 @@ public class AppleGame extends BaseEntity {
private Long sessionId;
private Long ownerId;
@Lob
private Board board;
@Convert(converter = AppleGameBoardConverter.class)
private AppleGameBoard appleGameBoard;
private int score = 0;
private LocalDateTime finishedAt;

public AppleGame(Board board, Long ownerId) {
this.board = board;
public AppleGame(AppleGameBoard appleGameBoard, Long ownerId) {
this.appleGameBoard = appleGameBoard;
this.ownerId = ownerId;
this.finishedAt = willFinishAt();
}

public AppleGame(Board board, Long ownerId, LocalDateTime finishedAt) {
this.board = board;
public AppleGame(AppleGameBoard appleGameBoard, Long ownerId, LocalDateTime finishedAt) {
this.appleGameBoard = appleGameBoard;
this.ownerId = ownerId;
this.finishedAt = finishedAt;
}

public AppleGame(Board board, Long ownerId, LocalDateTime finishedAt, int score) {
this.board = board;
public AppleGame(AppleGameBoard appleGameBoard, Long ownerId, LocalDateTime finishedAt, int score) {
this.appleGameBoard = appleGameBoard;
this.ownerId = ownerId;
this.finishedAt = finishedAt;
this.score = score;
}

public static AppleGame ofRandomized(Long ownerId) {
return new AppleGame(new Board(DEFAULT_HEIGHT, DEFAULT_WIDTH), ownerId);
return new AppleGame(new AppleGameBoard(DEFAULT_HEIGHT, DEFAULT_WIDTH), ownerId);
}

public void restart() {
validateOngoing();
this.board = board.reset();
this.appleGameBoard = appleGameBoard.reset();
this.score = 0;
this.createdAt = now();
this.finishedAt = willFinishAt();
}

public void removeApplesIn(Range range) {
validateOngoing();
List<Apple> removed = board.removeApplesIn(range);
List<Apple> removed = appleGameBoard.removeApplesIn(range);
if (removed.stream().anyMatch(Apple::isGolden)) {
board = board.reset();
appleGameBoard = appleGameBoard.reset();
}
score += removed.size();
}
Expand All @@ -102,7 +104,7 @@ private LocalDateTime willFinishAt() {
}

public List<List<Apple>> getApples() {
return board.getApples();
return appleGameBoard.getApples();
}

public void increase(int amount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {
public class AppleGameBoard {

private static final int REMOVABLE_SUM = 10;

private List<List<Apple>> apples;

public Board(List<List<Apple>> apples) {
public AppleGameBoard(List<List<Apple>> apples) {
validateSquared(apples);
this.apples = apples.stream()
.map(ArrayList::new)
.collect(Collectors.toList());
}

public Board(int height, int width) {
public AppleGameBoard(int height, int width) {
this(ApplesFactory.createRandomized(height, width));
}

Expand All @@ -39,8 +39,8 @@ private void validateSquared(List<List<Apple>> apples) {
}
}

public Board reset() {
return new Board(this.getHeight(), this.getWidth());
public AppleGameBoard reset() {
return new AppleGameBoard(this.getHeight(), this.getWidth());
}

protected List<Apple> removeApplesIn(Range range) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;

@Converter(autoApply = true)
public class BoardConverter implements AttributeConverter<Board, String> {
public class AppleGameBoardConverter implements AttributeConverter<AppleGameBoard, String> {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

Expand All @@ -19,18 +19,18 @@ public class BoardConverter implements AttributeConverter<Board, String> {
}

@Override
public String convertToDatabaseColumn(Board board) {
public String convertToDatabaseColumn(AppleGameBoard appleGameBoard) {
try {
return OBJECT_MAPPER.writeValueAsString(board);
return OBJECT_MAPPER.writeValueAsString(appleGameBoard);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

@Override
public Board convertToEntityAttribute(String dbJson) {
public AppleGameBoard convertToEntityAttribute(String dbJson) {
try {
return OBJECT_MAPPER.readValue(dbJson, Board.class);
return OBJECT_MAPPER.readValue(dbJson, AppleGameBoard.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import com.snackgame.server.applegame.controller.dto.GameResultResponse;
import com.snackgame.server.applegame.controller.dto.RangeRequest;
import com.snackgame.server.applegame.domain.game.AppleGame;
import com.snackgame.server.applegame.domain.game.AppleGameBoard;
import com.snackgame.server.applegame.domain.game.AppleGames;
import com.snackgame.server.applegame.domain.game.Board;
import com.snackgame.server.game.metadata.Metadata;
import com.snackgame.server.game.session.event.SessionEndEvent;

Expand All @@ -32,11 +32,11 @@ public AppleGame startGameFor(Long memberId) {

public Optional<AppleGame> placeMoves(Long memberId, Long sessionId, List<RangeRequest> rangeRequests) {
AppleGame game = appleGames.getBy(memberId, sessionId);
Board previous = game.getBoard();
AppleGameBoard previous = game.getAppleGameBoard();

rangeRequests.forEach(request -> game.removeApplesIn(request.toRange()));

if (!game.getBoard().equals(previous)) {
if (!game.getAppleGameBoard().equals(previous)) {
return Optional.of(game);
}
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.snackgame.server.game.snackgame.service

import com.snackgame.server.applegame.domain.game.Percentile

import com.snackgame.server.game.session.event.SessionEndEvent
import com.snackgame.server.game.session.exception.NoSuchSessionException
import com.snackgame.server.game.snackgame.domain.Percentile
import com.snackgame.server.game.snackgame.domain.SnackgameInfinite
import com.snackgame.server.game.snackgame.domain.SnackgameInifiniteRepository
import com.snackgame.server.game.snackgame.service.dto.SnackgameInfiniteEndResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.snackgame.server.game.snackgame.service.dto

import com.snackgame.server.applegame.domain.game.Percentile

import com.snackgame.server.game.session.domain.Session
import com.snackgame.server.game.session.service.dto.SessionResponse
import com.snackgame.server.game.snackgame.domain.Percentile
import com.snackgame.server.game.snackgame.domain.SnackgameInfinite
import io.swagger.v3.oas.annotations.media.Schema

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import com.snackgame.server.game.snackgame.service.SnackgameService
import com.snackgame.server.game.snackgame.service.dto.SnackgameEndResponse
import com.snackgame.server.game.snackgame.service.dto.SnackgameResponse
import com.snackgame.server.game.snackgame.service.dto.SnackgameUpdateRequest
import com.snackgame.server.game.snackgame.service.dto.StreakRequest
import com.snackgame.server.member.domain.Member
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
Expand Down Expand Up @@ -45,9 +48,31 @@ class SnackgameController(
fun update(
@Authenticated member: Member,
@PathVariable sessionId: Long,
@RequestBody request: @Valid SnackgameUpdateRequest,
@RequestBody @Valid request: SnackgameUpdateRequest,
): SnackgameResponse = snackgameService.update(member.id, sessionId, request)

@Operation(
summary = "스낵게임 세션 수 삽입",
description = """
지정한 세션에 수들을 삽입한다. 황금사과를 제거한 경우 초기화된 판을 응답한다.
"""

)
@PutMapping("/{sessionId}/moves")
fun placeMoves(
@Authenticated member: Member,
@PathVariable sessionId: Long,
@RequestBody requests: List<StreakRequest>
): ResponseEntity<SnackgameResponse> = snackgameService.placeMoves(member.id, sessionId, requests)
.map { game ->
ResponseEntity
.status(HttpStatus.CREATED)
.body(SnackgameResponse.of(game))
}
.orElseGet {
ResponseEntity.ok().build()
}

@Operation(
summary = "스낵게임 세션 일시정지",
description = """
Expand Down
84 changes: 84 additions & 0 deletions src/main/java/com/snackgame/server/game/snackgame/domain/Board.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.snackgame.server.game.snackgame.domain

import com.snackgame.server.game.snackgame.exception.InvalidBoardSizeException
import com.snackgame.server.game.snackgame.exception.InvalidCoordinateException
import com.snackgame.server.game.snackgame.exception.SnackNotRemovableException
import com.snackgame.server.game.snackgame.snack.EmptySnack
import com.snackgame.server.game.snackgame.snack.Snack
import java.util.stream.Collectors


class Board() {
private var snacks: MutableList<MutableList<Snack>> = arrayListOf()

constructor(snacks: MutableList<MutableList<Snack>>) : this() {
validateIsRectangle(snacks)
this.snacks = snacks.map { ArrayList(it) }.toMutableList()
}

constructor(height: Int, width: Int) : this(createRandomized(height, width))

private fun validateIsRectangle(snacks: List<List<Snack>>) {
if (snacks.isEmpty() || snacks[0].isEmpty()) {
throw InvalidBoardSizeException()
}
}

fun reset(): MutableList<MutableList<Snack>> {
return createRandomized(getHeight(), getWidth())
}

fun removeSnacksIn(streak: Streak): List<Snack> {
validateIsIncluded(streak.toCoordinates())
validateSumOf(streak.toCoordinates())

return streak.toCoordinates().stream().map(this::removeSnacksAt).filter(Snack::exists)
.collect(Collectors.toList())
}

private fun validateIsIncluded(coordinates: List<Coordinate>) {
coordinates.forEach { coordinate ->
if (coordinate.y >= snacks.size || coordinate.x >= snacks[0].size) {
throw InvalidCoordinateException()
}
}
}

private fun validateSumOf(coordinates: List<Coordinate>) {
if (sumSnacksIn(coordinates) != REMOVABLE_SUM) {
throw SnackNotRemovableException("스낵들의 합이 " + REMOVABLE_SUM + "이 아닙니다")
}
}

private fun sumSnacksIn(coordinates: List<Coordinate>): Int {
return getSnacksIn(coordinates).stream().map(Snack::getNumber).reduce(0, Integer::sum)

}

private fun getSnacksIn(coordinates: List<Coordinate>): List<Snack> {
return coordinates.stream().map { coordinate -> snacks[coordinate.y][coordinate.x] }
.collect(Collectors.toList())
}

private fun removeSnacksAt(coordinate: Coordinate): Snack {
val row = snacks[coordinate.y]
return row.set(coordinate.x, EmptySnack.get())
}

fun getSnacks(): List<List<Snack>> {
return snacks.map { ArrayList(it) }
}


private fun getHeight(): Int {
return this.snacks.size
}

private fun getWidth(): Int {
return this.snacks[0].size
}

companion object {
private const val REMOVABLE_SUM = 10;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.snackgame.server.game.snackgame.domain

import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.PropertyAccessor
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.ObjectMapper
import javax.persistence.AttributeConverter


class BoardConverter : AttributeConverter<Board, String> {

override fun convertToDatabaseColumn(board: Board): String {
return try {
OBJECT_MAPPER.writeValueAsString(board)
} catch (e: JsonProcessingException) {
throw RuntimeException(e)
}
}

override fun convertToEntityAttribute(dbJson: String): Board {
return try {
OBJECT_MAPPER.readValue(dbJson, Board::class.java)
} catch (e: JsonProcessingException) {
throw RuntimeException(e)
}
}

companion object {
private val OBJECT_MAPPER = ObjectMapper().apply {
setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
setVisibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.snackgame.server.game.snackgame.domain

import com.snackgame.server.game.snackgame.exception.NegativeCoordinateException


class Coordinate(val y: Int, val x: Int) {

init {
validateNonNegative(y)
validateNonNegative(x)
}


private fun validateNonNegative(axisCoordinate: Int) {
if (axisCoordinate < 0) {
throw NegativeCoordinateException()
}
}

override fun equals(o: Any?): Boolean {

if (this === o) return true
if (o == null || javaClass != o.javaClass) return false

val that = o as Coordinate

return if (y != that.y) false else x == that.x
}

override fun hashCode(): Int {
var result = y
result = 31 * result + x
return result
}

override fun toString(): String {
return "Coordinate{" +
"y=" + y +
", x=" + x +
'}'
}
}
Loading

0 comments on commit 54612a9

Please sign in to comment.