2013년 1월 20일 일요일

한글 입력 이론 (2)

지난번에 한글을 바꾸는 원리에 대해 제대로 설명하지 못한 것 같아서
개요를 짚고 넘어갑니다. 여기에는 버퍼도 포함합니다.

1. 주어진 스트링을 input라 하자.
2. 버퍼 스트링 buffer와 결과물 ret를 빈 스트링으로 선언
3. for i = 0부터 input.length()-1까지 반복
4. buffer에 input[i]를 붙인다
5. if buffer에 분할이 필요하면(아래 버퍼스트링이 변환 후 2글자 이상이 된다면)
(분할된 문자열 앞부분을 buffer.front 뒷부분을 buffer.back이라고 하면)
6. buffer.front를 한글로 변환해서 ret에 붙인다
7. buffer = buffer.back(버퍼에 남은 뒷부분을 저장한다)
8. endif
9. endfor
10. 버퍼에 남은 문자열을 한글로 변환해서 ret에 붙인다.
11. 결과물: ret

이해가 안 되시나요? 일반적인 한글 입력과 완전히 똑같은 의미입니다.
해설을 하자면,
5에서 '분할이 필요하면'이란 것은 지난번에 봤듯이 'rksk' 같은 걸 입력했을 때
'k'와 's' 사이에서 끊어야 ('가|나') 한다는 겁니다. 끊어야 할 필요가 없을 때는
버퍼에 데이터를 계속 추가(4번)하고, 끊어야 할 때는 끊어서 앞부분을 한글로 변환('가')
해서 붙이고(6번) 남은 'sk'를 buf에 저장(7번)한다는 겁니다.

분할이 필요 역할을 하는 함수가 needSeperating입니다.
이 함수는 분할이 필요할 경우 분할할 곳의 위치를,
아닐 경우 0을 리턴합니다.
한글로 변환 역할을 하는 함수가 assemble입니다.
전체적인 역할을 하는 함수를 translate라고 하면, 우리는 이제
translate의 코드를 짤 수 있습니다. 여러분이 한 번 해 보세요.


.

.

.

코드는 다음과 같습니다.

아, 참고로 저는 UTF-8에서 코드를 썼기 때문에 wstring 대신 string을 쓴 겁니다. wstring의 코드가 필요하시면 직접 변환(1분도 안 걸립니다.)하시거나 저한테 달라고 해 보세요.

1:  string translate(string input) {                        // 1
2:     int i, len = (int)input.length(), temp;
3:     string buffer, ret;                                 // 2
4:      for (i=0; i<len; ++i) {                             // 3
5:          buffer += input[i];                             // 4
6:          if ((temp = needSeperating(buffer))) {          // 5
7:              ret += assemble(buffer.substr(0, temp));    // 6
8: 
            buffer = buffer.substr(temp);               // 7
9: 
        }                                               // 8
10: 
    }                                                   // 9
11: 
    ret += assemble(buffer);                            // 10
12: 
    return ret;                                         // 11
13: 
}
14: 


#include <string>과 using namespace std;는 당연히 되어 있을 거라 생각하고
짠 코드입니다. 제가 저 위에서 얼마나 친절하게 설명을 했는지
보이시죠? 그냥 그대~로 코드를 짜면 됩니다.

이제 needSeperating의 코드를 봅시다. needSeperating은 특수문자가 보이면
'어, 끊어야 할 곳이네?'하고 끊어버립니다. 입력 원리를 그대로 적용하기 때문에 그렇습니다.
이건 지난 시간에 아~주 철저하게 설명을 했으니, 넘어가고 코드만 보겠습니다.

#define INT char
int needSeperating(string input) {
    int i, len = (int)input.length();
    int last = -1, temp;
    INT *arr;   // 0 : 자음(초성/종성 불확실) 1 : 모음

                // 2 : 확실한 종성 (다른 것과 조합 가능)
                // 3 : 자음(종성)과 조합 가능한 모음
                // 4 : 조합 불가능한 종성(포화상태)
    arr = (INT *)malloc(sizeof(INT) * len);
    for (i=0; i<len; ++i) {
        if (isJaum(input[i])) {
            *(arr + i) = 0;
        } else if (isMoum(input[i])) {
            *(arr + i) = 1;
        } else {                //특수문자가 보이면,
            if (i == 0) {       //첫번째에 특수문자가 있을 경우
                free(arr);      //메모리 관리
                return 1;       //1을 리턴한다. [1]
            } else {            //아닐 경우
                free(arr);      //메모리 관리
                return i;       //끊어야 할 곳을 리턴한다.
            }
        }
    }
    //여기서부터 i를 거치고 난 value 0은 확실한 초성이 된다.
    //검사를 하다 확실히 끊어야 할 상황이 되면 return
    for (i=0; i<len; ++i) {
        temp = *(arr + i);
        if (last == -1) {
            last = temp;
        } else {           //이전 거, 다음 거, if 문 처리가 막 보이죠?
            if (last == 0 && temp == 0) {
                free(arr);
                return i;
            } else if (last == 0 && temp == 1) {
                *(arr + i) = 3;
            } else if (last == 1 && temp == 0) {
                free(arr);
                return i;
            } else if (last == 1 && temp == 1) {
                if (!isAssemblableMoum(input.substr(i - 1, 2))) {
                    free(arr);
                    return i;
                }
            } else if (last == 2 && temp == 0) {
                if (!isAssemblableJaum(input.substr(i - 1, 2))) {
                    free(arr);
                    return i;
                } else {
                    *(arr + i) = 4;
                }
            } else if (last == 2 && temp == 1) {
                free(arr);
                return i - 1;
            } else if (last == 3 && temp == 0) {
                *(arr + i) = 2;
            } else if (last == 3 && temp == 1) {
                if (!isAssemblableMoum(input.substr(i - 1, 2))) {
                    free(arr);
                    return i;
                } else {
                    *(arr + i) = 3;
                }
            } else if (last == 4 && temp == 0) {
                free(arr);
                return i;
            } else if (last == 4 && temp == 1) {
                free(arr);
                return i - 1;
            }
            last = *(arr + i);
        }
    }
    free(arr);
    //검사를 해도 끊을 게 없다면 return 0
    return 0;
}


[1] 왜 1을 리턴할까요? 0을 리턴하면 '끊을 곳이 없다'라는 사인이 될 뿐만 아니라, 이미 끊어져 있는 부분을 또 끊어요? 말이 안 되죠. 그리고 특수문자 다음엔 어떤 것이 오든 그대로 2글자가 됩니다. (ex ' g' -> ' ㅎ')

컬러링 힘들었음 ㅠㅠ
isJaum, isMoum, isAssemblableJaum, isAssemblableMoum
(Assembleable인지 Assemblable인지 모르겠음)이라는 4개의 함수가 선언 없이 사용되었는데요, 이 함수들의 코드는 다음과 같습니다. 저 긴 코드에 비하면 진짜 간단해요 :D
bool isJaum(char input) {
    return (strchr("rRseEfaqQtTdwWczxvg", input) != NULL);
}
bool isMoum(char input) {
    return (strchr("koiOjpuPhynbml", input) != NULL);
}

bool isAssemblableJaum(string input) {
    string ok[] = {"rt", "sw", "sg", "fr", "fa", "fq", "ft", "fx", "fv", "fg", "qt"};
    int i;
    for (i=0; i<11; ++i) {
        if (input == ok[i]) {
            return true;
        }
    }
    return false;
}
bool isAssemblableMoum(string input) {

    string ok[] = {"hk", "hl", "ho", "nj", "nl", "np", "ml"};
    int i;
    for (i=0; i<7; ++i) {
        if (input == ok[i]) {
            return true;
        }
    }
    return false;
}

네. 이거에요. 그냥 레퍼런스 관리가 귀찮아서 함수화한 겁니다. 물론
저 Assemblable 시리즈는 약간 코드가 길지만 _로 분리해서 find로
훨씬 더 쉽게 할 수 있었어요. 다만 처리 속도가 길어지므로 pass

assemble은 합치는 건데요, 단일 한글(ex 'ㄱ', 'ㄴ', ..., 'ㅏ', 'ㅑ', ... 'ㅚ')은 받아서
바로 처리해주고, 완성 한글이라면 (ex '가', '한', '수')
맨 앞 한글자는 무조건 초성으로 하고
이후에 나오는 자음은 종성이,
이후에 나오는 모음은 중성이 되는 거죠. 두말 않고 코드를 보겠습니다.

string assemble(string input) {
    if (input.length() == 1) {   //길이가 1일 경우 무조건 단일
        string origin = "rRseEfaqQtTdwWczxvgkijuhynbmloOpP";
        string change

        = "ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅑㅓㅕㅗㅛㅜㅠㅡㅣㅐㅒㅔㅖ";
        int i, len = (int)origin.length();
        for (i=0; i<len; ++i) {
            if (origin[i] == input[0]) {
                return change.substr(i * 3, 3);     //UTF-8은 한글이 3칸이에요.
            }
        }
        return input;
    } else if (input.length() == 2 && isAssemblableMoum(input)) { // [1]

        string origin = "hkhlhonjnlnpml";
        string change = "ㅘㅚㅙㅝㅟㅞㅢ";
        int i, len = (int)origin.length() / 2;
        for (i=0; i<len; ++i) {
            if (input == origin.substr(i * 2, 2)) {
                return change.substr(i * 3, 3);       //UTF-8은 한글이 3칸이에요.
            }
        }
    } else {
        int cho = JaMoValue(input.substr(0, 1), 0), jung, jong;
        int unicode = 0xAC00;
        string ret;
        char UTF8Value[4];
        string temp = input.substr(1);
        int i;

        // 종성을 찾을 때까지 돌려서 중성/종성을 끊을 위치를 찾는 겁니다.
        for (i=0; i<input.length(); ++i) {
            if (isJaum(temp[i])) {
                break;
            }
        }
        jung = JaMoValue(temp.substr(0, i), 1);
        jong = JaMoValue(temp.substr(i), 2);
        temp = "";
        unicode += (cho * 588 + jung * 28 + jong);       //UTF16BE 형태
        UTF8Value[0] = 0xE0 + (unicode >> 12);
        UTF8Value[1] = 0x80 + ((unicode % 4096) >> 6);
        UTF8Value[2] = 0x80 + (unicode % 64);
        UTF8Value[3] = 0x00;
        ret.assign(UTF8Value);                           //UTF-8 형태
        return ret;
    }
    return input + "_";     //변환하지 못할 것을 대비해 끊을 곳만이라도 표시
}


[1] 종성, 중성 없이 초성만으로 ㄳ, ㄶ 만드는 건 잘못된 알고리즘입니다. 실제로 이렇게 쓰이는 경우가 거의 없어서 표준에서 뺐습니다. 저건 사실이지만 구현할 수도 있고 구현하기 귀찮다고는 말 못 해

간단하죠?
여기서 아무 선언 없이 쓰인 JaMoValue는 string과 int를 받습니다.
int가 0이면 초성 순서를, 1이면 중성 순서를, 2이면 종성 순서를 리턴합니다.
순서는 다음과 같습니다.


초성

01 23 45 67 89 1011 1213 1415 1617 18

중성

01 23 45 67 89 10
11 1213 1415 1617 1819 20

종성

X
0 1 2 3 4 5 6 7 8 9 10 11 12 13
14 15 16 17 18 19 20 21 22 23 24 25 26 27


표가 뒤죽박죽인 건 귀차니즘 때문에 소수점 안 쓴 거니 이해해주세요 :D
네. 보시다시피 한글 순서 그대로에요.
완성자의 조합 규칙은 이렇습니다.
44032 + (초성순서) * 588 + (중성순서) * 28 + (종성순서)
저 위에서 사용된 게 보이죠. 다시 얘기하지만
int형이 0이면 초성 순서, 1이면 중성 순서, 2면 종성 순서를
리턴합니다. 그렇다면 코드가 어떻게 짜여질지 눈에 선하게 보이죠?
물론 여기서는 한글로 했지만 내부적으로는 영어를 썼으므로
전부 영어로 (초성의 경우 'r=0 R=1 s=2 e=3 E=4'...) 해야겠죠.
코드는 다음과 같습니다.

int JaMoValue(string input, int value) {
    if (input.length() == 1) {
        string origin;
        if (value == 0) {
            origin = "rRseEfaqQtTdwWczxvg";
        } else if (value == 1) {
            origin = "koiOjpuPh///yn///bm/l";
        } else if (value == 2) {
            origin = "/rR/s//ef///////aq/tTdwczxvg";
        }
        int i, len = (int)origin.length();
        for (i=0; i<len; ++i) {
            if (origin[i] == input[0]) {
                return i;
            }
        }
    } else if (input.length() == 2) {

        string origin;
        //초성은 무조건 한 글자이므로 value 0 나오는 게 이상하죠.
        if (value == 1) {
            origin = "//////////////////hkhohl////njnpnl////ml";
        } else if (value == 2) {
            origin = "//////rt//swsg////frfafqftfxfvfg////qt";
        }
        int i, len = (int)origin.length();
        for (i=0; i<len; ++i) {
            if (origin.substr(i * 2, 2) == input) {
                return i;
            }
        }
    }
    return 0;
}


이해하셨나요? 여기까지 코드를 작성하고, 모든 함수에 대한 Prototype을 쓰신 다음,
main을 알아서들 쓰시고 컴파일해 보세요.
wstring으로 코드 바꾸는 건 메일하면 해 드립니다. :D
저는 메인을 이렇게 썼습니다: 이건 예제니 굳이 복붙하실 필요 없어요.

int main(int argc, char* argv[]) {
    string temp;
    while (1) {
        getline(cin, temp);
        cout << translate(temp) << endl;
    }
}


오늘은 어떻게... 강의 좀 잘 쓴 것 같나요?
너무 어렵게 설명한 건 아닌지... 약간 걱정이 됩니다.
다음 시간에는 진짜로 한글 입력 이론을 하겠습니다.
이건 string에서 data를 읽어서 buffer 처리를 한 것에 불과해요 :D

컬러링 잘못된 거 있으면 댓 달아주세요. 이게 댓 달면 메일이 오네 ㄷㄷ

2013년 1월 16일 수요일

2013년 1월 14일 월요일

2013년 1월 9일 수요일

현재 만들고 있는 리듬게임

여기에 swf가 링크가 제대로 안 되고
fps 나오는 것도 막장이라(저는 fps를 전부 120으로 맞춥니다.)
일단은 파일 다운로드 방식이어야겠네요.

다운로드

현재 버전은 1.1입니다.


저 링크 막 거는 사람 아니니까 믿으세요.
네이버 링크지만 광고 없고, 저런 건 받을수록 서버 과부하만 주니 지향해야 됩입니다.

리플레이가 없습니다.

실행법

플래시 플레이어가 있어야 됩니다. 하나 까시고
더블클릭하면 바로 실행됩니다.

조작법

오직! 숫자키 패드만 씁니다. (없으신 분은 위 숫자열을 누르셔도 되는데, 비추천.)
바깥쪽에서 다가오는 동그라미에 안쪽 동그라미가 일치하게 될 때,
안에 적힌 숫자눌러주세요!
그럼 됩니다.

그리고 상당히 매니악하게 되었는데
이유 분석을 해 보자면,

1. 누르는 순서가 숫자 순서가 아닙니다.
2. 공간 배열이 숫자 키패드와 다릅니다.

이 2가지 점 때문에 테스트할 때도 막 X뜨고 그랬어요.

플레이해보시고, 개선할 사항이나 지원할 것(금전적인 지원도 감사하구요, 그래픽/프로그래밍 알고리즘 등 어떤 것도 괜찮습니다.) 있으면 댓글을 달아요! 저 메일 그렇게 자주 못 봐요.

2013년 1월 3일 목요일

마인크래프트 정돌로 조작

보이는 것만 정돌로 바뀌는 겁니다.
아무리 이렇게 많이 바꿔도 새로고침 누르면 다시 되돌아옵니다.
로그인은 반드시 해야 하며, 여기에 회원가입한다고 돈 들어가는 거 아니니까 그냥 하세요.

중간에 인터셉트해서 (예: WireShark) 데이터 바꾸는 방법으로 할 수도 있는데 프록시 깔기 귀찮아서 그냥 했습니다.

MacBook Air와 Chrome을 사용했습니다. IE는 개발자 환경(F12)이고
FireFox 쓰시는 분들은 다 알 거라 생각합니다.
Safari는 없는 듯 합니다. 크롬 깔길 정말 잘 했네 ㅋ