개발/야추 다이스 (Yacht Dice)

점수 산출 알고리즘 구현하기

란서 2023. 3. 13. 16:19

 

점수 산출 방식

야추 다이스 보드게임은 플레이어가 굴린 5개의 주사위의 눈금에 따라 점수를 얻을 수 있다.
이 때 점수 산출 방식은 야추 다이스 보드게임만의 특별한 룰에 따른다.

야추 다이스의 점수 산출 방식은 다음 이미지와 링크를 참고한다. 
야추 다이스 점수 산출 방식

야추 다이스 점수 산출 방식.



따라서 해당 주사위 눈금에서 산출될 수 있는 모든 점수의 값을 구하고,

플레이어가 그 중에서 원하는 점수로 기록할 수 있도록 만들어야 한다.

 

점수 산출 알고리즘

YachtGame Class
class YachtGame @Inject constructor() {
    private val smalls = arrayOf(listOf(1,2,3,4), listOf(2,3,4,5), listOf(3,4,5,6))
    private val larges = arrayOf(listOf(1,2,3,4,5), listOf(2,3,4,5,6))
	
    fun getScore(...) : ..
	fun getRankScore(...) : ..
	suspend fun rollDice(..) : ..
}

 

점수를 산출할 수 있는 프로퍼티 및 메서드를 가진 클래스

 

 

suspend fun rollDice(Array<Int>, Array<Boolean>) : Array<Int>
suspend fun rollDice(dices:Array<Int>, keepIdx:Array<Boolean>) : Array<Int> = withContext(Dispatchers.Default) {
        //val random = SecureRandom.getInstanceStrong()
        val list1 = listOf(1,2,3,4,5,6)
        val list2 = listOf(1,2,3,4,5,6)
        val list3 = listOf(1,2,3,4,5,6)
        val list4 = listOf(1,2,3,4,5,6)
        val list5 = listOf(1,2,3,4,5,6)

        val array = arrayOf(
            if(!keepIdx[0]) list1.shuffled().first() else dices[0],
            if(!keepIdx[1]) list2.shuffled().first() else dices[1],
            if(!keepIdx[2]) list3.shuffled().first() else dices[2],
            if(!keepIdx[3]) list4.shuffled().first() else dices[3],
            if(!keepIdx[4]) list5.shuffled().first() else dices[4])

        array
    }


함수의 반환형은 Array이며, dices와 keepIdx 두 개의 매개변수를 받습니다. dices는 주사위의 현재 값이 저장된 배열이고, keepIdx는 어떤 주사위를 굴릴 것인지 선택하는 배열입니다.

withContext 함수를 사용하여 코루틴의 실행 컨텍스트를 지정합니다. 여기서는 Dispatchers.Default를 사용하여 백그라운드 스레드에서 실행되도록 합니다.

그 다음, list1부터 list5까지 1에서 6까지의 숫자를 포함하는 리스트를 만듭니다. 이 리스트들은 나중에 주사위를 굴릴 때 사용됩니다.

다음으로, array 배열을 만듭니다. 이 배열의 요소는 다음과 같이 만듭니다.

keepIdx[0]이 false이면 list1을 섞은 뒤 첫 번째 값을, true이면 dices[0]을 사용합니다.
keepIdx[1]이 false이면 list2을 섞은 뒤 첫 번째 값을, true이면 dices[1]을 사용합니다.
keepIdx[2]이 false이면 list3을 섞은 뒤 첫 번째 값을, true이면 dices[2]을 사용합니다.
keepIdx[3]이 false이면 list4을 섞은 뒤 첫 번째 값을, true이면 dices[3]을 사용합니다.
keepIdx[4]이 false이면 list5을 섞은 뒤 첫 번째 값을, true이면 dices[4]을 사용합니다.

최종적으로 array를 반환합니다.

따라서 이 함수는 keepIdx 배열에 따라 일부 주사위의 값을 유지하거나 새로운 값을 생성하여 반환합니다. 


fun getScore(Array<Int>) : Board // fun getRankScore(Array<Int>, Map<Int, Int>) : IntArray

해당 함수들은 게임에서 주어진 다이스를 이용하여 각각의 보드 점수를 계산하여 반환하는 함수입니다.

fun getScore(dices:Array<Int>) : Board {

        val numberCnt = dices.groupingBy { it }.eachCount()
        val ranks = getRankScore(dices, numberCnt)

        val result = IntArray(15){0}

        for((key,value) in numberCnt.toSortedMap(compareBy{ it })) {
            result[key-1] = (value*key)
        }

        result[8] = ranks[0]//choice
        result[9] = ranks[1]//fourCard
        result[10] = ranks[2]//fullHouse
        result[11] = ranks[3]//s.straight
        result[12] = ranks[4]//l.straight
        result[13] = ranks[5]//yacht


        //idx 6 = sum , idx 7 = bonus , idx 14 = total


        return Board(result)
    }

 

  1. getScore 함수: 다이스를 인자로 받아 각각의 보드 점수를 계산하여 반환하는 함수입니다.
    1. 숫자 카운트를 구합니다.
    2. getRankScore 함수를 호출하여 각 랭크 점수를 계산합니다.
    3. 1부터 6까지 각각의 숫자에 대해 등장 횟수를 곱한 값을 배열에 저장합니다.
    4. 8~13 인덱스에 getRankScore 함수에서 계산한 랭크 점수를 저장합니다.
    5. 6번 인덱스에는 1~6까지의 숫자 합을 저장합니다.
    6. 7번 인덱스에는 63점 이상인 경우 35점을, 그렇지 않은 경우 0점을 저장합니다.
    7. 14번 인덱스에는 모든 점수의 합을 저장합니다.
    8. 계산된 보드를 반환합니다.
    private fun getRankScore(dices: Array<Int>, numberCnt: Map<Int, Int>): IntArray {
        var ranks = intArrayOf()

        //1.chocie
        ranks += dices.sumOf { it }

        //2.4 of a kind
        val kind = numberCnt.filter { it.value >= 4 }.takeIf { it.isNotEmpty() }?.map { it.key }?.first() ?: 0
        ranks += if (kind > 0) {
            val notKind = dices.filter{ it != kind }.takeIf { it.isNotEmpty() }?.first() ?: kind
            (kind*4) + notKind
        } else 0

        //3.full house
        val triple = numberCnt.filter { it.value == 3}.takeIf { it.isNotEmpty() }?.map { it.key }?.first() ?: 0
        val twoPair = numberCnt.filter { it.value == 2}.takeIf { it.isNotEmpty() }?.map{it.key}?.first() ?: 0
        ranks += if(triple>0 && twoPair >0) triple*3 + twoPair*2 else 0

        //4.small Straight
        val s = dices.sorted().takeIf { it.containsAll(smalls[0]) || it.containsAll(smalls[1]) || it.containsAll(smalls[2])} !=null
        ranks += if(s) 15 else 0

        //5.large Straight
        val l = dices.sorted().takeIf { it.containsAll(larges[0]) || it.containsAll(larges[1]) } != null
        ranks += if(l) 30 else 0

        //6.yacht
        val yacht = numberCnt.filter { it.value == 5}.takeIf { it.isNotEmpty() } != null
        ranks += if(yacht) 50 else 0


        return ranks
    }
  1. getRankScore 함수: 다이스와 각 숫자별 등장 횟수를 받아서 보드 점수 중에서 랭크 점수를 계산하여 반환하는 함수입니다.
    1. choice: 다이스의 모든 값을 더한 값을 반환합니다.
    2. 4 of a kind: 다이스 중 4개의 값이 모두 같은 경우 4개 값의 합과 다른 값 하나를 더한 값을 반환합니다.
    3. full house: 다이스 중 3개의 값이 같고 2개의 값이 같은 경우 3개 값과 2개 값의 합을 반환합니다.
    4. small straight: 다이스의 값 중 작은 스트레이트(1,2,3,4 또는 2,3,4,5)가 있으면 15를 반환합니다.
    5. large straight: 다이스의 값 중 큰 스트레이트(1,2,3,4,5 또는 2,3,4,5,6)가 있으면 30을 반환합니다.
    6. yacht: 다이스 중 모든 값이 같은 경우 50을 반환합니다.




실행

fun main() {
    val yachtGame = YachtGame()

    var keepList = listOf<Int>()

    for(chance in 1..3) {
        keepList = yachtGame.rollDice(keepList, 5-keepList.size, chance)
    }

    println(yachtGame.solution(keepList))

}

Keep 기능을 간단하게 구현하여 위의 클래스를 실행한 결과

[6, 2, 2, 2, 4]
keep하고 싶은 주사위의 개수와 idx를 입력
keep하고 싶은 주사위의 개수 : 3
0 1 2

[6, 2, 2, 6, 4]
keep하고 싶은 주사위의 개수와 idx를 입력
keep하고 싶은 주사위의 개수 : 4
0 1 2 3

[6,2,2,6,1]
1: 1
2 : 4
6 : 12
choice : 17
four of a kind : 0
full house : 0
s.Straight : 0
l.Straight : 0
yacht : 0

소스 코드 

class YachtGame @Inject constructor() {
    private val smalls = arrayOf(listOf(1,2,3,4), listOf(2,3,4,5), listOf(3,4,5,6))
    private val larges = arrayOf(listOf(1,2,3,4,5), listOf(2,3,4,5,6))

    /**
     * 주사위를 k개를 굴려 나온 숫자들을 list(+keep)에 담아서 반환.
     * */
    suspend fun rollDice(dices:Array<Int>, keepIdx:Array<Boolean>) : Array<Int> = withContext(Dispatchers.Default) {
        //val random = SecureRandom.getInstanceStrong()
        val list1 = listOf(1,2,3,4,5,6)
        val list2 = listOf(1,2,3,4,5,6)
        val list3 = listOf(1,2,3,4,5,6)
        val list4 = listOf(1,2,3,4,5,6)
        val list5 = listOf(1,2,3,4,5,6)

        val array = arrayOf(
            if(!keepIdx[0]) list1.shuffled().first() else dices[0],
            if(!keepIdx[1]) list2.shuffled().first() else dices[1],
            if(!keepIdx[2]) list3.shuffled().first() else dices[2],
            if(!keepIdx[3]) list4.shuffled().first() else dices[3],
            if(!keepIdx[4]) list5.shuffled().first() else dices[4])

        array
    }

    fun getScore(dices:Array<Int>) : Board {

        val numberCnt = dices.groupingBy { it }.eachCount()
        val ranks = getRankScore(dices, numberCnt)

        val result = IntArray(15){0}

        for((key,value) in numberCnt.toSortedMap(compareBy{ it })) {
            result[key-1] = (value*key)
        }

        result[8] = ranks[0]//choice
        result[9] = ranks[1]//fourCard
        result[10] = ranks[2]//fullHouse
        result[11] = ranks[3]//s.straight
        result[12] = ranks[4]//l.straight
        result[13] = ranks[5]//yacht


        //idx 6 = sum , idx 7 = bonus , idx 14 = total


        return Board(result)
    }

    private fun getRankScore(dices: Array<Int>, numberCnt: Map<Int, Int>): IntArray {
        var ranks = intArrayOf()

        //1.chocie
        ranks += dices.sumOf { it }

        //2.4 of a kind
        val kind = numberCnt.filter { it.value >= 4 }.takeIf { it.isNotEmpty() }?.map { it.key }?.first() ?: 0
        ranks += if (kind > 0) {
            val notKind = dices.filter{ it != kind }.takeIf { it.isNotEmpty() }?.first() ?: kind
            (kind*4) + notKind
        } else 0

        //3.full house
        val triple = numberCnt.filter { it.value == 3}.takeIf { it.isNotEmpty() }?.map { it.key }?.first() ?: 0
        val twoPair = numberCnt.filter { it.value == 2}.takeIf { it.isNotEmpty() }?.map{it.key}?.first() ?: 0
        ranks += if(triple>0 && twoPair >0) triple*3 + twoPair*2 else 0

        //4.small Straight
        val s = dices.sorted().takeIf { it.containsAll(smalls[0]) || it.containsAll(smalls[1]) || it.containsAll(smalls[2])} !=null
        ranks += if(s) 15 else 0

        //5.large Straight
        val l = dices.sorted().takeIf { it.containsAll(larges[0]) || it.containsAll(larges[1]) } != null
        ranks += if(l) 30 else 0

        //6.yacht
        val yacht = numberCnt.filter { it.value == 5}.takeIf { it.isNotEmpty() } != null
        ranks += if(yacht) 50 else 0


        return ranks
    }
}

 

.