C++ Mersenne Twister 엔진을 활용한 로또 번호 자동 생성 및 CSV 저장

문제 정의

복권(예: 한국 로또 6/45 또는 더블록스)은 일반적으로 일정 범위 내에서 중복 없는 숫자 조합을 요구합니다. 본 프로그램은 사용자가 직접 선정한 후보 번호 그룹(예: 3개 그룹)에서 각 그룹별로 지정된 개수만큼 숫자를 무작위로 뽑아 하나의 복권 번호 세트를 완성합니다. 생성된 5세트의 번호는 콘솔에 출력됨과 동시에 CSV(Comma-Separated Values) 파일로 저장되어 엑셀과 같은 스프레드시트 프로그램에서 추가 분석이 가능하도록 합니다.

설계 접근법

이 프로그램의 핵심 기능은 두 가지입니다: 고품질의 난수 생성과 사용자 친화적인 상호작용.

  • 고급 난수 생성: C++11 표준 라이브러리의 <random> 헤더를 사용하여 결정론적 의사 난수를 생성합니다. 단순히 rand() 함수를 사용하는 것보다 더 강력하고 예측 불가능한 시퀀스를 생성하기 위해 std::mt19937_64 (64비트 Mersenne Twister 엔진)을 채택했습니다. 또한, 시간, 프로세스 ID, 스레드 ID 및 std::random_device를 결합하여 초기 시드(seed)의 엔트로피(무작위성)를 극대화했습니다.
  • 모듈식 설계: 각 기능(난수 생성, 복권 번호 조합, 사용자 입력, 파일 저장)은 독립적인 함수로 분리하여 코드의 가독성과 재사용성을 높였습니다.
  • 사용자 상호작용: 사용자는 세 개의 후보 번호 그룹을 각각 입력할 수 있습니다. 프로그램은 각 그룹에서 뽑을 숫자의 개수를 입력받아 합계가 총 볼 개수(예: 6개)가 되도록 검증합니다. 이후 Enter 키를 누를 때마다 5세트의 새로운 번호를 생성하며, ESC 키를 누르면 프로그램이 종료됩니다.

1. 핵심 난수 생성 함수: getRandomSubset

이 함수는 주어진 정수 벡터에서 중복 없이 count개의 요소를 무작위로 선택합니다. Fisher-Yates 셔플 알고리즘의 변형을 사용하여 리스트를 무작위로 섞은 후, 앞에서부터 필요한 개수만큼 가져오는 방식을 사용합니다.

#include <vector>
#include <algorithm> // for std::shuffle
#include <random>
#include <chrono>
#include <thread>
#include <windows.h>
// ... (다른 include)

std::vector<int> getRandomSubset(const std::vector<int>& source, size_t count) {
    if (count > source.size()) {
        throw std::invalid_argument("요청 개수가 소스 벡터 크기보다 큽니다.");
    }

    // 1. 작업 복사본 생성
    std::vector<int> temp = source;

    // 2. 엔트로피가 높은 시드 생성
    auto now = std::chrono::high_resolution_clock::now();
    auto nanosec = std::chrono::time_point_cast<std::chrono::nanoseconds>(now).time_since_epoch().count();
    DWORD processId = GetCurrentProcessId();
    std::thread::id threadId = std::this_thread::get_id();
    std::random_device hardwareRandomness;
    std::seed_seq seed_seq{
        static_cast<unsigned int>(nanosec),
        static_cast<unsigned int>(processId),
        static_cast<unsigned int>(std::hash<std::thread::id>()(threadId)),
        hardwareRandomness()
    };

    // 3. 64비트 Mersenne Twister 엔진 초기화
    std::mt19937_64 generator(seed_seq);

    // 4. 엔진 워밍업 (엔진 상태를 더욱 무작위화)
    for (int warmup = 0; warmup < 100; ++warmup) {
        generator();
    }

    // 5. Fisher-Yates 스타일 셔플 수행
    for (size_t i = temp.size() - 1; i > 0; --i) {
        std::uniform_int_distribution<size_t> distribution(0, i);
        size_t j = distribution(generator);
        std::swap(temp[i], temp[j]);
    }

    // 6. 셔플된 리스트의 처음 'count'개 요소 추출
    std::vector<int> result(temp.begin(), temp.begin() + count);
    std::sort(result.begin(), result.end()); // 결과를 오름차순으로 정렬
    return result;
}

2. 복권 번호 조합 함수: composeLotteryTicket

이 함수는 세 개의 후보 그룹과 각 그룹에서 뽑을 개수를 입력받아 하나의 티켓(빨간색 공)을 완성합니다.

std::vector<int> composeLotteryTicket(const std::vector<int>& groupA,
                                        const std::vector<int>& groupB,
                                        const std::vector<int>& groupC,
                                        size_t countA, size_t countB, size_t countC) {
    std::vector<int> ticket;
    
    // 각 그룹에서 난수 부분 집합 추출
    std::vector<int> subsetA = getRandomSubset(groupA, countA);
    std::vector<int> subsetB = getRandomSubset(groupB, countB);
    std::vector<int> subsetC = getRandomSubset(groupC, countC);
    
    // 모든 번호를 하나의 티켓으로 병합
    ticket.insert(ticket.end(), subsetA.begin(), subsetA.end());
    ticket.insert(ticket.end(), subsetB.begin(), subsetB.end());
    ticket.insert(ticket.end(), subsetC.begin(), subsetC.end());
    
    // 최종 오름차순 정렬 (복권 번호 표시 형식)
    std::sort(ticket.begin(), ticket.end());
    return ticket;
}

3. 보너스 볼 생성 함수: generateBonusBall

별도의 난수 엔진을 사용하여 1에서 16 사이의 값을 생성합니다.

int generateBonusBall() {
    auto now = std::chrono::high_resolution_clock::now();
    auto nanosec = std::chrono::time_point_cast<std::chrono::nanoseconds>(now).time_since_epoch().count();
    DWORD processId = GetCurrentProcessId();
    std::seed_seq seed{ static_cast<unsigned int>(nanosec), static_cast<unsigned int>(processId) };
    std::mt19937_64 gen(seed);

    for (int i = 0; i < 50; ++i) {
        gen();
    }
    std::uniform_int_distribution<int> distribution(1, 16);
    return distribution(gen);
}

4. 사용자 입력 인터페이스

사용자는 콘솔을 통해 숫자를 입력합니다. inputNumberGroup 함수는 프롬프트를 출력하고, 유효성 검사(1-33 범위, 최대 33개)를 수행하며 입력을 벡터로 저장합니다.

std::vector<int> inputNumberGroup(const std::string& prompt) {
    std::vector<int> group;
    int inputNum;
    std::cout << prompt << std::endl;
    while (std::cin >> inputNum) {
        if (group.size() >= 33) {
            std::cout << "최대 33개의 숫자만 입력 가능합니다." << std::endl;
            break;
        }
        if (inputNum < 1 || inputNum > 33) {
            std::cout << "1에서 33 사이의 숫자만 입력해주세요." << std::endl;
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            continue;
        }
        group.push_back(inputNum);
        if (std::cin.peek() == '\n') break;
    }
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    return group;
}

5. 출력 및 저장 함수

displayAndSaveTickets 함수는 5세트의 티켓을 생성하고, 표준 출력(콘솔)과 파일 스트림에 동시에 CSV 형식으로 기록합니다.

void displayAndSaveTickets(std::ofstream& outFile,
                            const std::vector<int>& gA, const std::vector<int>& gB, const std::vector<int>& gC,
                            size_t nA, size_t nB, size_t nC) {
    for (int t = 0; t < 5; ++t) {
        std::vector<int> reds = composeLotteryTicket(gA, gB, gC, nA, nB, nC);
        int bonus = generateBonusBall();

        // 콘솔 출력
        for (size_t i = 0; i < reds.size(); ++i) {
            std::cout << std::setfill('0') << std::setw(2) << reds[i];
            if (i < reds.size() - 1) std::cout << " ";
        }
        std::cout << " | " << std::setfill('0') << std::setw(2) << bonus << std::endl;

        // CSV 파일 기록
        for (size_t i = 0; i < reds.size(); ++i) {
            outFile << std::setfill('0') << std::setw(2) << reds[i];
            if (i < reds.size() - 1) outFile << ",";
        }
        outFile << "," << std::setfill('0') << std::setw(2) << bonus << std::endl;
    }
}

6. 메인 함수 흐름 제어

메인 함수는 프로그램의 전체적인 흐름을 관리합니다.

int main() {
    // 1. 사용자로부터 숫자 그룹 입력받기
    std::vector<int> group1 = inputNumberGroup("첫 번째 번호 그룹을 입력하세요 (1~33):");
    std::vector<int> group2 = inputNumberGroup("두 번째 번호 그룹을 입력하세요 (1~33):");
    std::vector<int> group3 = inputNumberGroup("세 번째 번호 그룹을 입력하세요 (1~33):");

    // 2. 총 그룹 크기 검증 (6 ~ 33 사이)
    while ( (group1.size() + group2.size() + group3.size() < 6) ||
            (group1.size() + group2.size() + group3.size() > 33) ) {
        std::cout << "세 그룹의 총 숫자 개수는 6에서 33 사이여야 합니다. 다시 입력하세요.\n";
        group1 = inputNumberGroup("첫 번째 그룹:");
        group2 = inputNumberGroup("두 번째 그룹:");
        group3 = inputNumberGroup("세 번째 그룹:");
    }

    // 3. 각 그룹에서 뽑을 개수 입력 및 검증
    size_t pickA, pickB, pickC;
    do {
        std::cout << "각 그룹에서 뽑을 개수를 입력하세요 (합계 = 6): ";
        std::cin >> pickA >> pickB >> pickC;
    } while (pickA + pickB + pickC != 6);

    // 4. CSV 파일 열기 및 헤더 쓰기
    std::ofstream file("lottery_output.csv");
    file << "R1,R2,R3,R4,R5,R6,Bonus\n";

    // 5. 키보드 입력 루프 (Enter: 생성, ESC: 종료)
    std::cout << "Enter 키를 누르면 5세트가 생성됩니다. ESC 키를 누르면 종료됩니다.\n";
    while (true) {
        int key = _getch();
        if (key == 27) { // ESC
            break;
        } else if (key == '\r' || key == '\n') { // Enter
            displayAndSaveTickets(file, group1, group2, group3, pickA, pickB, pickC);
        }
    }

    file.close();
    return 0;
}

전체 소스 코드

다음은 위에서 설명한 모든 구성 요소를 포함하는 완전한 프로그램입니다. Visual Studio 2019 환경에서 컴파일 및 실행을 확인했습니다.

// ... (모든 include와 함수 정의를 순서대로 포함)
// (위에 정의된 getRandomSubset, composeLotteryTicket, 
//  generateBonusBall, inputNumberGroup, displayAndSaveTickets, main)

태그: C++ Mersenne Twister mt19937_64 CSV 난수 생성

7월 19일 05:57에 게시됨