Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[STEP 4] πŸ’£μ§€λ’°μ°ΎκΈ°πŸ’₯ λ¦¬νŒ©ν„°λ§ #492

Open
wants to merge 47 commits into
base: y2gcoder
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
67d6034
feature(minesweeper): λ‹«νžŒ 셀은 μ—΄λ¦° μƒνƒœκ°€ μ•„λ‹˜
y2gcoder Dec 24, 2024
87f9a81
feature(minesweeper): 지뒰 셀은 μ—΄λ¦° μƒνƒœκ°€ 맞음
y2gcoder Dec 24, 2024
fcf55c6
feature(minesweeper): 숫자 셀은 μ—΄λ¦° μƒνƒœκ°€ 맞음
y2gcoder Dec 24, 2024
e795376
feature(minesweeper): 지뒰 셀은 μ—΄λ©΄ 지뒰 셀이닀
y2gcoder Dec 24, 2024
785ab43
feature(minesweeper): 숫자 셀은 μ—΄λ©΄ 숫자 셀이닀
y2gcoder Dec 24, 2024
15419fa
feature(minesweeper): 지뒰가 μ—†λŠ” λ‹«νžŒ 셀을 μ—΄λ©΄ 숫자 셀이 λœλ‹€
y2gcoder Dec 24, 2024
cbef00b
feature(minesweeper): 지뒰가 μžˆλŠ” λ‹«νžŒ 셀을 μ—΄λ©΄ 지뒰 셀이 λœλ‹€
y2gcoder Dec 24, 2024
7e95717
feature(minesweeper): μ§€λ’°λŠ” 인접 지뒰 수λ₯Ό 0 ~ 8κΉŒμ§€ κ°€μ§ˆ 수 μžˆλ‹€
y2gcoder Dec 24, 2024
18a37e0
refactor(minesweeper): 지뒰 μ…€μ˜ 이름을 MineCell 둜 λ³€κ²½ && 인접 지뒰 μˆ˜λ„ AdjacentMi…
y2gcoder Dec 24, 2024
fae4162
feature(minesweeper): 지뒰가 없을 λ•Œ λ‹«νžŒ 셀을 μ—΄λ©΄ λ˜‘κ°™μ€ μΈμ ‘ν•œ 지뒰 수λ₯Ό 가진 숫자 셀이 λœλ‹€
y2gcoder Dec 24, 2024
167164e
feature(minesweeper): AdjacentMines κ°’ 객체 생성 및 적용
y2gcoder Dec 24, 2024
4c4d663
feature(minesweeper): 각 셀듀은 지뒰가 μžˆλŠ”μ§€ μ•Œ 수 μžˆλ‹€
y2gcoder Dec 24, 2024
5f25ce3
feature(minesweeper): λ³΄λ“œλŠ” μ…€ λͺ©λ‘μ„ λ°›μ•„ 생성할 수 μžˆλ‹€
y2gcoder Dec 24, 2024
873606a
feature(minesweeper): λ³΄λ“œλŠ” μ˜μ—­κ³Ό μ…€ λͺ©λ‘μ„ λ°›μ•„ 생성할 수 μžˆλ‹€
y2gcoder Dec 25, 2024
53bf394
feature(minesweeper): 높이, λ„ˆλΉ„, 지뒰 개수λ₯Ό λ°›μ•„ λ³΄λ“œλ₯Ό λ§Œλ“€ 수 μžˆλ‹€
y2gcoder Dec 25, 2024
1cbe37d
feature(minesweeper): 높이, λ„ˆλΉ„, 지뒰 개수λ₯Ό λ°›μ•„ λ³΄λ“œλ₯Ό λ§Œλ“€λ©΄ 지뒰 개수만큼의 지뒰λ₯Ό 가진 λ‹«νžŒ 셀이…
y2gcoder Dec 25, 2024
3595790
feature(minesweeper): 인접 지뒰 μˆ˜λŠ” 1μ”© 증가할 수 있음 && 인접 지뒰 μˆ˜λΌλ¦¬λŠ” μ„œλ‘œ 비ꡐ할 수 있음
y2gcoder Dec 25, 2024
bf4da51
feature(minesweeper): λ‹«νžŒ 셀은 인접 지뒰 수λ₯Ό μ—…λ°μ΄νŠΈν•  수 있음
y2gcoder Dec 25, 2024
346b425
feature(minesweeper): BoardBuilder λŠ” λ„ˆλΉ„, 높이, 지뒰 κ°œμˆ˜μ™€ μˆ˜λ™ 지뒰 μœ„μΉ˜λ₯Ό λ°›μ•„ λ³΄λ“œλ₯Ό …
y2gcoder Dec 26, 2024
6a01bd9
feature(minesweeper): BoardBuilder μ—μ„œ λ³΄λ“œνŒμ„ 생성할 λ•Œ 지뒰가 μ•„λ‹Œ 셀듀에 인접 지뒰 수λ₯Ό …
y2gcoder Dec 26, 2024
b88f181
refactor(minesweeper): BoardBuilderλ₯Ό dsl νŒ¨ν‚€μ§€λ‘œ 이동
y2gcoder Dec 26, 2024
7db19b6
feature(minesweeper): Boardλ₯Ό DSL둜 μƒμ„±ν•˜κΈ° μœ„ν•΄ BoardCreator.board(), @Boar…
y2gcoder Dec 26, 2024
066477a
feature(minesweeper): λ³΄λ“œ μ…€ 생성 μ±…μž„μ„ BoardCellsCreator 둜 μœ„μž„
y2gcoder Dec 26, 2024
99b27e6
feature(minesweeper): μœ„μΉ˜μ— λŒ€ν•œ 인접 8λ°©ν–₯ μœ„μΉ˜λ₯Ό κ΅¬ν•˜λŠ” μ±…μž„μ„ μ§€λ‹Œ AdjacentDirection μΆ”κ°€
y2gcoder Dec 26, 2024
3d23580
refactor(minesweeper): DefaultBoardCellsCreator λ¦¬νŒ©ν† λ§
y2gcoder Dec 26, 2024
35a1b1f
refactor(minesweeper): Builder λ₯Ό dsl νŒ¨ν‚€μ§€λ‘œ 이동
y2gcoder Dec 26, 2024
85882d5
refactor(minesweeper): BoardCellsCreator 와 κ΅¬ν˜„μ²΄λ₯Ό strategy νŒ¨ν‚€μ§€λ‘œ 이동
y2gcoder Dec 26, 2024
7d0f692
feature(minesweeper): λ³΄λ“œλŠ” λ³΄λ“œ 내에 μ—†λŠ” μœ„μΉ˜μ˜ 셀을 μ—΄λ €κ³  ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό 던짐
y2gcoder Dec 26, 2024
17d508f
feature(minesweeper): BoardBuilder, BoardCreator에 openAt()을 μΆ”κ°€ν•˜μ—¬ μˆ˜λ™μœΌλ‘œβ€¦
y2gcoder Dec 26, 2024
1c667cf
test(minesweeper): λ³΄λ“œμ˜ 이미 μ—΄λ¦° 셀을 μ—΄λ©΄ λ³΄λ“œμ˜ λ³€ν™”κ°€ μ—†μŒ
y2gcoder Dec 27, 2024
e89e17a
feature(minesweeper): λ³΄λ“œμ—μ„œ 지뒰λ₯Ό 가진 λ‹«νžŒ 셀을 μ—΄λ©΄ ν•΄λ‹Ή μ…€λ§Œ μ—°λ‹€
y2gcoder Dec 27, 2024
b0abb91
feature(minesweeper): λ³΄λ“œμ—μ„œ μ΄μ›ƒν•œ 8λ°©ν–₯ 쀑에 지뒰가 μžˆλŠ” 셀을 μ—΄λ©΄ ν•΄λ‹Ή μ…€λ§Œ μ—΄λ¦°λ‹€
y2gcoder Dec 27, 2024
92b9dfa
feature(minesweeper): Location 에 인덱슀둜 λ°”κΎΈλŠ” 것과 인덱슀λ₯Ό Location으둜 λ°”κΎΈλŠ” λ©”μ„œλ“œ μΆ”κ°€
y2gcoder Dec 28, 2024
8b6285c
test(minesweeper): μ΄μ›ƒν•œ 8λ°©ν–₯ 쀑에 지뒰가 μ—†λŠ” 셀을 μ—΄λ©΄ μ΄μ›ƒν•œ 지뒰가 μ—†λŠ” 셀을 λͺ¨λ‘ μ—°λ‹€
y2gcoder Dec 28, 2024
f1d4677
feature(minesweeper): μœ„μΉ˜κ°€ λ³΄λ“œ μ˜μ—­ 내에 μžˆλŠ” μœ νš¨ν•œ μœ„μΉ˜μΈμ§€ 체크할 수 μžˆλŠ” isValid(area:…
y2gcoder Dec 28, 2024
34f8cd4
feature(minesweeper): 셀이 인접 μ…€κΉŒμ§€ ν™•μž₯ν•΄μ„œ μ—΄ 수 μžˆλŠ” 셀인지 ν™•μΈν•˜λŠ” isExpandableToAd…
y2gcoder Dec 28, 2024
2015588
refactor(minesweeper): Board 의 open λ¦¬νŒ©ν„°λ§
y2gcoder Dec 28, 2024
b87b41b
feature(minesweeper): μ—΄μ–΄μ•Ό ν•  μ…€ μœ„μΉ˜ λͺ©λ‘μ„ κ³„μ‚°ν•˜λŠ” μ±…μž„μ„ ShouldOpenLocationFinde…
y2gcoder Dec 28, 2024
a1aed5b
feature(minesweeper): Game은 높이, λ„ˆλΉ„, 지뒰 개수λ₯Ό λ°›μ•„ μƒμ„±ν•œ λ³΄λ“œλ₯Ό 가지고 λ§Œλ“€μ–΄μ§„λ‹€
y2gcoder Dec 28, 2024
ed5a968
feature(minesweeper): λ³΄λ“œλŠ” λ‹«νžŒ μ…€ 수λ₯Ό ꡬ할 수 μžˆλ‹€
y2gcoder Dec 28, 2024
c7cd98a
feature(minesweeper): λ³΄λ“œλŠ” λ‹«νžŒ μ…€ 수λ₯Ό ꡬ할 수 μžˆλ‹€
y2gcoder Dec 28, 2024
ce91b02
feature(minesweeper): λ³΄λ“œμ˜ λ‹«νžŒ μ…€ μˆ˜μ™€ 총 지뒰 μˆ˜κ°€ κ°™μœΌλ©΄ κ²Œμž„μ€ μŠΉλ¦¬ν•œ 것이닀
y2gcoder Dec 28, 2024
855aa14
feature(minesweeper): κ²Œμž„μ˜ 패배, 계속 쑰건 μΆ”κ°€
y2gcoder Dec 28, 2024
7cd9ec2
feature(minesweeper): κ²Œμž„ ν”Œλ ˆμ΄λ₯Ό μœ„ν•œ ui, 메인 클래슀 μΆ”κ°€
y2gcoder Dec 28, 2024
22e236f
refactor(minesweeper): ConsoleInputView 에 μœ„μΉ˜ μž…λ ₯ ν›„ ν•œ μΉΈ λ„μš°λ„λ‘ println() μΆ”κ°€
y2gcoder Dec 28, 2024
905cf24
feature(minesweeper): OutputView 에 μ—λŸ¬ λ©”μ‹œμ§€ 좜λ ₯용 printError μΆ”κ°€
y2gcoder Dec 28, 2024
13b4815
feature(minesweeper): μž…λ ₯κ°’ μœ νš¨μ„± 검사 μ‹€νŒ¨μ‹œ μž¬μ‹œλ„ 처리
y2gcoder Dec 28, 2024
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
16 changes: 16 additions & 0 deletions src/main/kotlin/tdd/minesweeper/Main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package tdd.minesweeper

import tdd.minesweeper.ui.ConsoleInputView
import tdd.minesweeper.ui.ConsoleOutputView
import tdd.minesweeper.ui.MineSweeperApp
import tdd.minesweeper.ui.RetryingInputView

fun main() {
val app =
MineSweeperApp(
inputView = RetryingInputView(ConsoleInputView),
outputView = ConsoleOutputView,
)

app.play()
}
19 changes: 19 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/AdjacentDirection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package tdd.minesweeper.domain

enum class AdjacentDirection(val dr: Int, val dc: Int) {
TOP_LEFT(-1, -1),
TOP(-1, 0),
TOP_RIGHT(-1, 1),
LEFT(0, -1),
RIGHT(0, 1),
BOTTOM_LEFT(1, -1),
BOTTOM(1, 0),
BOTTOM_RIGHT(1, 1),
;

companion object {
fun allAdjacentLocations(location: Location): List<Location> {
return entries.map { Location(location.row + it.dr, location.col + it.dc) }
}
}
}
21 changes: 21 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/AdjacentMines.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package tdd.minesweeper.domain

@JvmInline
value class AdjacentMines(private val value: Int) : Comparable<AdjacentMines> {
init {
require(value in MIN_VALUE..MAX_VALUE) {
"인접 지뒰 μˆ˜λŠ” $MIN_VALUE ~ $MAX_VALUE 만 ν—ˆμš©ν•œλ‹€: valuer=$value"
}
}

fun inc(): AdjacentMines {
return AdjacentMines(value + 1)
}

override operator fun compareTo(other: AdjacentMines): Int = value.compareTo(other.value)

companion object {
private const val MIN_VALUE = 0
private const val MAX_VALUE = 8
}
}
12 changes: 12 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/Area.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tdd.minesweeper.domain

data class Area(val height: Int, val width: Int) {
init {
require(height > 0) {
"λ†’μ΄λŠ” μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€: height=$height"
}
require(width > 0) {
"λ„ˆλΉ„λŠ” μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€: width=$width"
}
}
}
37 changes: 37 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/Board.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tdd.minesweeper.domain

import tdd.minesweeper.domain.strategy.DefaultShouldOpenLocationFinder
import tdd.minesweeper.domain.strategy.ShouldOpenLocationFinder

data class Board(
val area: Area,
val cells: Cells,
private val shouldOpenLocationFinder: ShouldOpenLocationFinder = DefaultShouldOpenLocationFinder(),
) {
fun countOfClosed(): Int = cells.count { !it.isOpen() }

fun countOfMineOpened(): Int = cells.count { it is MineCell }

fun open(location: Location): Board {
validateLocation(location)

val shouldOpen = shouldOpenLocationFinder.findAllShouldOpen(this, location)

return this.copy(cells = applyOpen(shouldOpen))
}

private fun validateLocation(location: Location) {
require(location.isValid(area)) {
"λ³΄λ“œ λ‚΄μ˜ μœ„μΉ˜κ°€ μ•„λ‹™λ‹ˆλ‹€: location=$location"
}
}

private fun applyOpen(shouldOpen: Set<Location>): Cells {
val shouldOpenIndexes = shouldOpen.map { it.toIndex(area.width) }.toSet()
val result = cells.toMutableList()

shouldOpenIndexes.forEach { index -> result[index] = cells[index].open() }

return Cells(result.toList())
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/Cell.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package tdd.minesweeper.domain

interface Cell {
val adjacentMines: AdjacentMines?

fun isOpen(): Boolean

fun hasMine(): Boolean

fun open(): Cell

fun isExpandableToAdjacent(): Boolean
}
4 changes: 4 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/Cells.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package tdd.minesweeper.domain

@JvmInline
value class Cells(private val values: List<Cell>) : List<Cell> by values
25 changes: 25 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/ClosedCell.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package tdd.minesweeper.domain

data class ClosedCell(
private val hasMine: Boolean = false,
override val adjacentMines: AdjacentMines = AdjacentMines(0),
) : Cell {
constructor(adjacentMines: Int) : this(adjacentMines = AdjacentMines(adjacentMines))

override fun isOpen(): Boolean = false

override fun hasMine(): Boolean = hasMine

override fun open(): Cell {
if (hasMine) {
return MineCell
}
return NumberCell(adjacentMines)
}

override fun isExpandableToAdjacent(): Boolean {
return !hasMine() && adjacentMines == AdjacentMines(0)
}

fun withAdjacentMines(newAdjacentMines: AdjacentMines): ClosedCell = this.copy(adjacentMines = newAdjacentMines)
}
35 changes: 35 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/Game.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package tdd.minesweeper.domain

import tdd.minesweeper.domain.dsl.board

class Game(private val countOfMines: Int, board: Board) {
var board = board
private set

fun open(location: Location) {
board = board.open(location)
}

fun state(): GameState =
when {
board.countOfMineOpened() > 0 -> GameState.LOSE
countOfMines == board.countOfClosed() -> GameState.WIN
else -> GameState.CONTINUE
}

companion object {
fun from(
height: Int,
width: Int,
countOfMines: Int,
): Game {
val board =
board {
height(height)
width(width)
countOfMines(countOfMines)
}
return Game(countOfMines, board)
}
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/GameState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package tdd.minesweeper.domain

enum class GameState {
WIN,
LOSE,
CONTINUE,
}
20 changes: 20 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/Location.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package tdd.minesweeper.domain

data class Location(val row: Int, val col: Int) {
fun toIndex(width: Int): Int = (row - 1) * width + (col - 1)

fun isValid(area: Area): Boolean {
return row in 1..area.height && col in 1..area.width
}

companion object {
fun from(
index: Int,
width: Int,
): Location =
Location(
row = (index / width) + 1,
col = (index % width) + 1,
)
}
}
13 changes: 13 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/MineCell.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package tdd.minesweeper.domain

object MineCell : Cell {
override val adjacentMines: AdjacentMines? = null

override fun isOpen(): Boolean = true

override fun hasMine(): Boolean = true

override fun open(): Cell = this

override fun isExpandableToAdjacent(): Boolean = false
}
11 changes: 11 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/NumberCell.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package tdd.minesweeper.domain

data class NumberCell(override val adjacentMines: AdjacentMines = AdjacentMines(0)) : Cell {
override fun isOpen(): Boolean = true

override fun hasMine(): Boolean = false

override fun open(): Cell = this

override fun isExpandableToAdjacent(): Boolean = false
}
91 changes: 91 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/dsl/BoardBuilder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package tdd.minesweeper.domain.dsl

import tdd.minesweeper.domain.Area
import tdd.minesweeper.domain.Board
import tdd.minesweeper.domain.Cells
import tdd.minesweeper.domain.Location
import tdd.minesweeper.domain.strategy.BoardCellsCreator
import tdd.minesweeper.domain.strategy.DefaultBoardCellsCreator

@BoardDslMaker
class BoardBuilder(private val boardCellsCreator: BoardCellsCreator = DefaultBoardCellsCreator()) :
Builder<Board> {
private var height: Int = 0
private var width: Int = 0
private var countOfMines: Int = 0
private val manualMineLocations: MutableSet<Location> = mutableSetOf()
private val manualOpenLocations: MutableSet<Location> = mutableSetOf()

fun height(value: Int) =
apply {
require(value > 0) { "λ†’μ΄λŠ” μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€! input=$value" }
this.height = value
}

fun width(value: Int) =
apply {
require(value > 0) { "λ†’μ΄λŠ” μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€! input=$value" }
this.width = value
}

fun countOfMines(value: Int) = apply { this.countOfMines = value }

fun mineAt(
row: Int,
col: Int,
) = apply { this.manualMineLocations.add(Location(row, col)) }

fun openAt(
row: Int,
col: Int,
) = apply { this.manualOpenLocations.add(Location(row, col)) }

override fun build(): Board {
require(height > 0) { "λ†’μ΄λŠ” μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€! height=$height" }
require(width > 0) { "λ†’μ΄λŠ” μ–‘μˆ˜μ—¬μ•Ό ν•©λ‹ˆλ‹€! width=$width" }
require(countOfMines <= width * height) { "μ΅œλŒ€ 지뒰 κ°œμˆ˜λŠ” λͺ¨λ“  μ…€μ˜ μˆ˜μž…λ‹ˆλ‹€! max=${width * height}, countOfMines=$countOfMines" }

val area = Area(height = height, width = width)

val cells =
boardCellsCreator.createCells(
area = area,
countOfMines = countOfMines,
inputManualMineLocations = manualMineLocations.toSet(),
)

if (manualOpenLocations.isEmpty()) {
return Board(
area = area,
cells = cells,
)
}

val openedCells: Cells = openCellsManually(cells, area)

return Board(
area = area,
cells = openedCells,
)
}

private fun openCellsManually(
cells: Cells,
area: Area,
): Cells {
return Cells(
cells.mapIndexed { index, cell ->
val location =
Location.from(
index = index,
width = area.width,
)
if (location in manualOpenLocations) {
cell.open()
} else {
cell
}
},
)
}
}
8 changes: 8 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/dsl/BoardCreator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package tdd.minesweeper.domain.dsl

import tdd.minesweeper.domain.Board

@DslMarker
annotation class BoardDslMaker

fun board(block: BoardBuilder.() -> Unit): Board = BoardBuilder().apply(block).build()
5 changes: 5 additions & 0 deletions src/main/kotlin/tdd/minesweeper/domain/dsl/Builder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package tdd.minesweeper.domain.dsl

interface Builder<T> {
fun build(): T
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package tdd.minesweeper.domain.strategy

import tdd.minesweeper.domain.Area
import tdd.minesweeper.domain.Cells
import tdd.minesweeper.domain.Location

interface BoardCellsCreator {
fun createCells(
area: Area,
countOfMines: Int,
inputManualMineLocations: Set<Location>,
): Cells
}
Loading