CafeM0ca

[Go] 로또 시뮬레이터 구현 본문

Programming/프로젝트

[Go] 로또 시뮬레이터 구현

M0ca 2022. 7. 22. 07:05
반응형

로또 시뮬레이션 프로젝트(Golang)

로또 6/45는 1부터 45까지의 수 중에서 중복없이 6개를 뽑아 숫자를 맞추는 게임이다.

확률은 다음과 같다.

  • 1등: 814만5060분의 1 (0.000000122773804% -> 0.00000012%)
  • 2등: 135만7510분의 1 (0.000000736642824% -> 0.00000074%)
  • 3등 3만5724분의 1 (0.000027992386071 -> 0.000028%)
  • 4등 733분의 1 (0.001364256480218% -> 0.00136%)
  • 5등 45분의 1 (0.022222222222222% -> 0.02%)

성인이 되고나서 로또 몇장을 나와 연관 있는 숫자로 찍기도 하고 꿈에서 나온 숫자를 조합해서 찍어보기도 했는데 1등은 커녕 5등 한번도 당첨되본 기억이 없다. (한번에 최대치인 10만원을 샀지만 말이다)

로또를 안사지만 간단하게 문법만 익힌 상태에서 로또 시뮬레이터를 구현하는건 꽤 할만한 토이 프로젝트라고 생각되어 이 글에서는 Golang으로 로또 시뮬레이터를 구현하는 방법에 대해서 설명한다.

이 글에서 사용하는 golang 버전은 1.18이다.

우선 프로젝트 디렉토리를 생성하고 go 프로젝트 진행을 위해 초기화해준다.

mkdir lotto_simulator
cd lotto_simulator
go mod init lotto_simulator
mkdir cmd src
touch cmd/main.go

cmd 디렉토리는 실행 파일이 생성될 공간이고 src는 프로젝트 관련 소스코드들을 저장할 디렉토리다.

로또는 수동으로 번호를 선택하는 것과 자동으로 번호를 선택하는 2가지 방법이 있다.
이를 구현하는 lotto.go 파일을 src안에 만들어준다.

touch src/lotto/lotto.go

우선 수동으로 번호를 뽑아주는 기능부터 만들어보자

func NewGame() []int {
    fmt.Println("1~45중에 숫자 6개를 중복되지 않도록 뽑으세요.")
    numbers := []int{}

    // 수동으로 번호 6개를 뽑을 때 까지 반복
    for len(numbers) < 6 {
        var num int
        fmt.Printf("현재 입력된 번호들\n\t - %v\n", numbers)
        fmt.Printf("%d번째 번호 입력:", len(numbers)+1)

        // 입력 예외 처리
        _, err := fmt.Scanf("%d", &num)
        if err != nil || num < 1 || num > 45 {
            fmt.Println("잘못된 입력")
            continue
        }

        // 이미 뽑은 번호를 중복해서 뽑은 경우 예외 처리
        if lotto_util.Contain(numbers, num) {
            fmt.Println("이미 존재하는 번호입니다. 재입력하세요.")
            continue
        }
        numbers = append(numbers, num)

        // 매번 정렬할 필요는 없지만 화면에 깔끔하게 보여지기 위해 정렬
        sort.Ints(numbers)
    }
    fmt.Printf("입력한 로또 번호: %v\n", numbers)
    return numbers
}

뽑은 번호가 이미 뽑은 번호인지 확인하기 위해 Contain(slice, element)라는 함수를 구현해볼 것이다.

mkdir src/lotto_util
touch src/lotto_util/contain.go

Go 1.18부터 generic이라는 개념이 추가 되었다.

// contain.go
package lotto_util

import "golang.org/x/exp/constraints"

// constraints.Ordered는 순서가 비교 가능한 타입들로 Integer Float stirng이 있다.
// < <= >= > 연산을 지원하는 타입들이다.
// https://pkg.go.dev/golang.org/x/exp/constraints#Ordered

func Contain[T constraints.Ordered](slices []T, target T) bool {
    for _, element := range slices {
        if element == target {
            return true
        }
    }
    return false
}

Contain함수는 target이 slices에 존재하는지 확인하기 위해 반복문을 돌며 원소를 찾을 것이다.

제대로 작동하는지 unit test를 진행해보자.

touch src/lotto_util/contain_test.go
package lotto_util

import "testing"

func TestContain(t *testing.T) {
    result := Contain([]int{1, 2, 3, 4, 5}, 5)
    expected := true
    if result != expected {
        t.Fail()
    }
    result2 := Contain([]float64{1.0, 2.2, 3.3}, 3.14)
    expected2 := false
    if result2 != expected2 {
        t.Fail()
    }
    result3 := Contain([]string{"alice", "bob", "jiny"}, "jiny")
    expected3 := true
    if result3 != expected3 {
        t.Fail()
    }
}

test는 각각 int slice, float slice, string slice으로 제네릭이 잘 작동하는지 판단하기 위해 간단하게 구성해보았다.

> cd src/lotto_util
> go test

PASS
ok      lotto_simulator/src/lotto_util    0.322s

다시 src/lotto로 돌아와서 이번에는 자동으로 번호 6개를 생성하는 기능을 구현해보자

// lotto.go

// NewGame(){} ...

func GenerateRandomLottoNumberForUser() []int {
    fmt.Println("자동 로또 번호 생성중 . . .")
    var newLottoNumber []int 
    for len(newLottoNumber) < 6 {
        // 1 <= num <= 45
        num := rand.Intn(45)+1
        if lotto_util.Contain(newLottoNumber, num) {
            continue
        }
        newLottoNumber = append(newLottoNumber, num)
    }
    sort.Ints(newLottoNumber)
    fmt.Printf("\t생성된 사용자 로또 번호: %v\n", newLottoNumber)
    return newLottoNumber
}

수동과 비슷하지만 입력 부분 대신 rand.Intn()함수를 통해 번호를 선택하고 아까 구현한 Contain함수를 사용하여 중복체크를 진행하는 모습이다.

이제 사용자가 자동과 수동으로 번호를 뽑는 것은 구현되었고 컴퓨터가 번호를 뽑을 차례다.
컴퓨터는 6개의 번호와 2등을 구분하기 위한 보너스 번호를 뽑는다.
이를 위해 GenerateRandomLottoNumberForUser() 함수를 살짝 변경해주자.

// lotto.go

type ComputerLottoNumber struct {
    Numbers []int
    Bonus int        
}

// NewGame() ...
// GenerateRandomLottoNumberForUser() ...

// 컴퓨터가 생성한 로또 번호
func GenerateRandomLottoNumberForComputer() ComputerLottoNumber {
    fmt.Println("컴퓨터 로또 번호 생성중 . . .")
    var newLottoNumber ComputerLottoNumber
    for len(newLottoNumber.Numbers) < 6 {
        // 1 <= num <= 45
        num := rand.Intn(45)+1
        if lotto_util.Contain(newLottoNumber.Numbers, num) {
            continue
        }
        newLottoNumber.Numbers = append(newLottoNumber.Numbers, num)
    }
    sort.Ints(newLottoNumber.Numbers)

    // 보너스 번호 뽑기
    for {
        // 1 <= num <= 45
        num := rand.Intn(45)+1
        if lotto_util.Contain(newLottoNumber.Numbers, num) {
            continue
        }
        newLottoNumber.Bonus = num
        break
    }

    fmt.Printf("\t생성된 로또 번호: %v, 보너스 번호:%d\n", newLottoNumber.Numbers, newLottoNumber.Bonus)
    return newLottoNumber
}

사용자가 (자동/수동으로)뽑은 번호와 컴퓨터가 뽑은 번호를 비교하여 등수를 정하는 함수를 만들어보자.

// lotto.go
func CompareLottoNumber(userLotto []int, computerLotto *ComputerLottoNumber) int {
    if len(userLotto) != 6 || len(computerLotto.Numbers) != 6 {
        panic("lotto number of Numbers should be 6")
    }

    same := 0
    // userLotto에서 번호를 하나씩 뽑아서 컴퓨터가 뽑은 번호에 몇개나 해당하는지 체크한다.
    for _, num := range userLotto {
        if lotto_util.Contain(computerLotto.Numbers, num) {
            same++    
        }
    }
    if same == 6 {
        return 1
    }
    // 5개 맞은 경우 2등인지 확인을 위해 보너스 번호가 사용자의 로또 번호에 있는지 호가인
    if same == 5 && lotto_util.Contain(userLotto, computerLotto.Bonus) {
        return 2
    }
    if same <= 2 { //낙첨
        return 0 
    }
    return 8-same // 3~5등
}

여기까지 lotto.go를 작성해봤다. 전체 코드는 아래와 같다.

// lotto.go
package lotto

import (
    "fmt"
    "math/rand"
    "sort"

    "lotto_simulator/src/lotto_util"
)

type ComputerLottoNumber struct {
    Numbers []int
    Bonus   int
}

func NewGame() []int {
    fmt.Println("1~45중에 숫자 6개를 중복되지 않도록 뽑으세요.")
    numbers := []int{}

    for len(numbers) < 6 {
        var num int
        fmt.Printf("현재 입력된 번호들\n\t - %v\n", numbers)
        fmt.Printf("%d번째 번호 입력:", len(numbers)+1)
        _, err := fmt.Scanf("%d", &num)
        if err != nil || num < 1 || num > 45 {
            fmt.Println("잘못된 입력")
            continue
        }
        if lotto_util.Contain(numbers, num) {
            fmt.Println("이미 존재하는 번호입니다. 재입력하세요.")
            continue
        }
        numbers = append(numbers, num)
        sort.Ints(numbers)
    }
    fmt.Printf("입력한 로또 번호: %v\n", numbers)
    return numbers
}

func GenerateRandomLottoNumberForUser() []int {
    fmt.Println("자동 로또 번호 생성중 . . .")
    var newLottoNumber []int
    for len(newLottoNumber) < 6 {
        // 1 <= num <= 45
        num := rand.Intn(45) + 1
        if lotto_util.Contain(newLottoNumber, num) {
            continue
        }
        newLottoNumber = append(newLottoNumber, num)
    }
    sort.Ints(newLottoNumber)
    fmt.Printf("\t생성된 사용자 로또 번호: %v\n", newLottoNumber)
    return newLottoNumber
}

// 컴퓨터가 생성한 로또 번호
func GenerateRandomLottoNumberForComputer() ComputerLottoNumber {
    fmt.Println("컴퓨터 로또 번호 생성중 . . .")
    var newLottoNumber ComputerLottoNumber
    for len(newLottoNumber.Numbers) < 6 {
        // 1 <= num <= 45
        num := rand.Intn(45) + 1
        if lotto_util.Contain(newLottoNumber.Numbers, num) {
            continue
        }
        newLottoNumber.Numbers = append(newLottoNumber.Numbers, num)
    }
    sort.Ints(newLottoNumber.Numbers)
    for {
        // 1 <= num <= 45
        num := rand.Intn(45) + 1
        if lotto_util.Contain(newLottoNumber.Numbers, num) {
            continue
        }
        newLottoNumber.Bonus = num
        break
    }

    fmt.Printf("\t생성된 로또 번호: %v, 보너스 번호:%d\n", newLottoNumber.Numbers, newLottoNumber.Bonus)
    return newLottoNumber
}

func CompareLottoNumber(userLotto []int, computerLotto *ComputerLottoNumber) int {
    if len(userLotto) != 6 || len(computerLotto.Numbers) != 6 {
        panic("lotto number of Numbers should be 6")
    }

    same := 0
    for _, num := range userLotto {
        if lotto_util.Contain(computerLotto.Numbers, num) {
            same++
        }
    }
    if same == 6 {
        return 1
    }
    if same == 5 && lotto_util.Contain(userLotto, computerLotto.Bonus) {
        return 2
    }
    if same <= 2 { //낙첨
        return 0
    }
    return 8 - same // 3~5등
}

로또 등수가 각 경우에 적합하게 나오는지 확인을 위해 유닛 테스트를 진행하자.

touch src/lotto/lotto_test.go
//lotto_test.go
package lotto

import "testing"

func TestCompareLottoNumber(t *testing.T) {
    type args struct {
        userLotto     []int
        computerLotto ComputerLottoNumber
    }
    tests := []struct {
        name string `json:name`
        args args   `json:args`
        want int    `json:want`
    }{
        { // 구조체 순서에 맞춰 초기화
            "1등 테스트",
            args{
                []int{1, 2, 3, 4, 5, 6},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    7,
                },
            },
            1,
        },
        { // json으로 구조체 초기화
            name: "2등 테스트",
            args: args{
                []int{1, 2, 3, 4, 5, 7},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    7,
                },
            },
            want: 2,
        },
        {
            name: "3등 테스트",
            args: args{
                []int{1, 2, 3, 4, 5, 7},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    8,
                },
            },
            want: 3,
        },
        {
            name: "4등 테스트",
            args: args{
                []int{1, 2, 3, 4, 9, 7},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    8,
                },
            },
            want: 4,
        },
        {
            name: "5등 테스트",
            args: args{
                []int{1, 2, 3, 11, 12, 13},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    8,
                },
            },
            want: 5,
        },
        {
            name: "낙첨 테스트1",
            args: args{
                []int{1, 2, 14, 11, 12, 13},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    8,
                },
            },
            want: 0,
        },
        {
            name: "낙첨 테스트2",
            args: args{
                []int{1, 15, 16, 11, 12, 13},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    8,
                },
            },
            want: 0,
        },
        {
            name: "낙첨 테스트3",
            args: args{
                []int{40, 15, 16, 11, 12, 13},
                ComputerLottoNumber{
                    []int{1, 2, 3, 4, 5, 6},
                    8,
                },
            },
            want: 0,
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := CompareLottoNumber(tt.args.userLotto, &tt.args.computerLotto); got != tt.want {
                t.Errorf("CompareLottoNumber() = %v, want %v", got, tt.want)
            }
        })
    }
}
> cd src/lotto
> go test
PASS
ok      lotto_simulator/src/lotto    0.268s

자 이제 main함수에서 구현된 함수들을 제어하는 인터페이스를 구현할 차례다.
main.go는 cmd/main.go에 처음에 만들어 놨었다.

// main.go
package main

import (
    "fmt"

    "lotto_simulator/src/lotto"
)

func main() {
    fmt.Println("Lotto Simulator")
    for {
        fmt.Println("1. 자동 구매\n2. 수동 구매\n3. 종료")
        var menu int
        for {
            fmt.Print("메뉴 선택(1 or 2 or 3):")
            _, err := fmt.Scanf("%d", &menu)
            if err != nil {
                fmt.Println("\t잘못된 입력")
                continue
            }
            if menu < 1 || menu > 3 {
                continue
            } 
            if menu == 3 {
                return
            }
            break
        }

        fmt.Print("게임 횟수 입력(최대 10000회):")
        var times int
        for {
            _, err := fmt.Scanf("%d", &times)
            if err != nil {
                fmt.Print("잘못된 입력 다시 입력하세요:")
                continue
            }
            if times < 1 || times > 10000 {
                fmt.Print("잘못된 입력 다시 입력하세요:")
                continue
            } 
            break

        }

        computerLotto := lotto.GenerateRandomLottoNumberForComputer()
        var gameResult [6]int
        switch menu {
        case 1:
            var autoGameResult [][]int
            for i:=1; i<=times; i++ {
                fmt.Println(i, "번째 게임")
                autoGameResult = append(autoGameResult, lotto.GenerateRandomLottoNumberForUser())
            }

            for i, game := range autoGameResult {
                result := lotto.CompareLottoNumber(game, &computerLotto)
                fmt.Printf("당첨 번호:%v 보너스 번호:%d\n", computerLotto.Numbers, computerLotto.Bonus)    
                if result == 0 {
                    fmt.Println(i+1, "번째 게임:", game, "결과:낙첨")
                } else {
                    fmt.Println(i+1, "번째 게임:", game, "결과:", result, "등")
                }
                gameResult[result]++
            }
        case 2:
            var selfGameResult [][]int
            for i:=1; i<=times; i++ {
                fmt.Println(i, "번째 게임")
                selfGameResult = append(selfGameResult, lotto.NewGame())
            }

            for i, game := range selfGameResult {
                result := lotto.CompareLottoNumber(game, &computerLotto)
                fmt.Printf("당첨 번호:%v 보너스 번호:%d\n", computerLotto.Numbers, computerLotto.Bonus)    
                if result == 0 {
                    fmt.Println(i+1, "번째 게임:", game, "결과:낙첨")
                } else {
                    fmt.Println(i+1, "번째 게임:", game, "결과:", result, "등")
                }
                gameResult[result]++
            }
        }
        fmt.Println("결과 정산")
        fmt.Println("구매한 게임 수:", times)
        for i:=1; i<=5; i++ {
            fmt.Printf("%d등: %d번\n", i, gameResult[i])
        }
        fmt.Printf("낙첨: %d번\n", gameResult[0])
    }
}

여기까지 로또 시뮬레이터를 구현해봤다.

아래의 명령어로 실행해보자.

> cd cmd
> go build -o lotto_simulator
> ./lotto_simulator

혹시 특정 패키지(constraints)가 없다고 뜨면 go 컴파일러가 친절하게 없으니까 설치하라고 알려줄 것이다.
에러 메시지를 잘 보고 그대로 복사 붙여넣기 하자.

프로젝트 전체 소스코드는 여기서(https://github.com/jiny0x01/go_playground/tree/main/lotto_simulator) 확인할 수 있다.

반응형

'Programming > 프로젝트' 카테고리의 다른 글

[프로젝트] Hyu 시연 영상  (0) 2020.12.04
[해커톤] z-wave  (0) 2019.07.24
[C++] hyu Note generator  (0) 2018.07.16
[C++] Tic Tac Toe  (1) 2018.07.09
[Java] FileAttributeViewr  (0) 2018.06.24
Comments