보드지를 잘라 쓰던 그 카드를 그대로 화면에 옮기는 게 출발이었어요. Flutter로 앱 껍데기를 세우고, 카드를 끌어 옮기는 부분은 Flame에 맡겼습니다. 문법 판정은 처음엔 문장 템플릿 목록을 두고 손패가 그중 하나에 맞는지 보는 방식이었어요. 캐릭터 리액션, 이모트, 효과음 21종, 진동까지 붙여서 한 바퀴는 돌렸습니다.


바로 플레이하거나, 처음이면 배우기로 갑니다

혼자 할 것과 친구와 할 것을 갈라서 세웁니다

일곱 장을 받고 덱에서 한 장 가져오며 차례가 시작됩니다

카드를 뽑고 나면 문장을 밀어 올릴 수 있게 됩니다

손패에서 밀어 올린 카드가 그대로 문장이 됩니다

아홉 걸음을 따라가며 한 판으로 규칙을 익힙니다

밝기를 고르고 효과음·배경 음악·진동을 조절합니다
어떤 서비스인가요?
영어 단어가 한 장씩 적힌 카드를 손에 들고, 어순에 맞게 늘어놓아 문장을 만드는 카드 게임이에요. 손에 든 카드를 먼저 다 털어낸 사람이 이깁니다.
어떤 계기로 만들게 됐나요?
어렸을 적 다니던 영어학원에서 이 카드 게임을 했어요. 그때는 보드지를 잘라 만든 카드를 보드게임처럼 돌렸습니다. 그게 재밌었던 기억이 남아 있어서, 앱으로 만들어 보고 싶었어요.
주로 어떤 분이 쓰나요?
연령 구분 없이 영어를 배우는 사람 모두예요. 처음에는 6~10세 아이만 생각하고 만들다가, 도중에 어른 학습자까지 넓혔습니다.
만들면서 배운 게 있다면요?
답안 판정 로직이 제일 빡셌어요. 문장이 맞았는지 틀렸는지를 무엇으로 가를지, 그 한 줄을 정하는 데 제일 오래 걸렸습니다.
혼자 배울지, AI와 붙을지, 친구를 부를지 정합니다.
덱 맨 위나 버림 더미 맨 위에서 한 장을 뽑습니다. 뽑기 전에는 아무것도 할 수 없어요.
카드를 옆으로 밀면 손패 안에서 순서가 바뀌고, 위로 밀면 테이블에 놓입니다.
버튼을 누르면 문법 엔진이 판정합니다.
먼저 비운 사람이 이깁니다.
보드지를 잘라 쓰던 그 카드를 그대로 화면에 옮기는 게 출발이었어요. Flutter로 앱 껍데기를 세우고, 카드를 끌어 옮기는 부분은 Flame에 맡겼습니다. 문법 판정은 처음엔 문장 템플릿 목록을 두고 손패가 그중 하나에 맞는지 보는 방식이었어요. 캐릭터 리액션, 이모트, 효과음 21종, 진동까지 붙여서 한 바퀴는 돌렸습니다.
한 바퀴 돌려 보니 규칙에 큰 구멍이 있었습니다. 최소 다섯 장짜리 문장만 인정했는데, 무작위로 받은 일곱 장에서 다섯 장짜리 문장이 나오는 일이 거의 없었어요. AI는 매 턴 카드만 뽑고, 사람도 마찬가지고, 게임이 93턴쯤 돌다가 덱이 떨어져서 끝났습니다.
그래서 규칙을 러미로 갈아엎었어요. 한 장 뽑고 → 한 가지 동작. 버림 더미를 두고, 덱이 떨어지면 버림 더미를 섞어 되넣고. 최소 길이 제한은 없앴습니다. 긴 문장일수록 손이 빨리 비니까, 규칙으로 강제하지 않아도 구조가 알아서 긴 문장을 보상하더라고요.
여기서 카드를 버리는 걸 의무로 두면 안 된다는 것도 알게 됐어요. 턴을 끝내려면 반드시 버려야 한다면, 한 장 뽑고 한 장 버리는 사람은 손패 크기가 영원히 그대로라 절대 못 이깁니다. 그래서 패스를 진짜 선택지로 넣었어요.
템플릿 목록은 늘려도 늘려도 부족했습니다. 문장 하나를 더 받아주려고 목록에 한 줄 더 넣는 짓을 반복하다가, 그냥 영문법 책에 있는 대로 재귀 청크 파서를 짰어요. 명사구, 형용사구, 전치사구, 동사구를 각각 정의하고 문장은 그 조합으로 봅니다.
그 위에 규칙을 다섯 개 얹었어요. 관사 일치, 수 일치, 주어-동사 일치, 형용사 어순, 그리고 동사 자릿수(valency)요. 자릿수를 넣고 나서야 apples run a friend 같은 게 걸리기 시작했습니다. run은 목적어를 못 받는다는 건 의미가 아니라 문법이니까요.
기능은 되는데 화면이 학습 앱처럼 생겼더라고요. 디자인 토큰을 따로 패키지로 빼고 전부 다시 그렸습니다. 종이는 브랜드 색을, 가구(테이블·레일)는 크림색을 갖는 걸로 재료를 두 개만 뒀어요.
처음엔 초록 펠트에 황동 테두리를 둘렀는데, 황동이 화면의 모든 선에 노란 물을 들이더라고요. 황동을 빼고 크림으로 바꿨습니다. 그런데 초록 테이블에 스포트라이트가 남아 있으니 여전히 카지노처럼 보였어요. 결국 초록도 빼고 색이 아예 없는 #15181C 바닥으로 갔습니다. 화면에 색이 있는 건 카드에 인쇄된 것뿐이 됐어요.
카드 앞면도 다시 잡았습니다. 단어를 가운데에 놓으면 손패가 겹쳤을 때 맨 위 한 장만 읽혀요. 그래서 전부 왼쪽에 붙였습니다. 겹친 카드가 보여주는 건 왼쪽 끝이니까, 그렇게 하면 일곱 장이 다 읽혀요. 이걸 하고 나니 인쇄된 테두리도, 양쪽 구석의 인덱스도, 단어와 뜻 사이의 줄도 전부 필요가 없어져서 지웠습니다.
나중에 낮 모드도 붙였습니다. 안내문과 설정을 종이에서 걷어내 바닥에 직접 얹고 나니, 이 앱이 밝은 쪽이 아예 없는 어두운 앱이 돼 있더라고요. 그런데 두 재료 중 움직이는 건 바닥뿐입니다. 바닥은 방의 불빛이 정하는 것이지만 카드는 인쇄물이라, 불을 켠다고 다시 인쇄되지는 않으니까요. 그래서 테이블·레일·바닥 위의 글자만 바뀌고 카드는 그대로예요. 낮의 바닥은 회색이 아니라 카드 종이와 같은 흰색입니다. 회색으로 먼저 해봤는데 그건 밝은 화면이 아니라 어두운 화면에 불만 켠 꼴이라 버렸어요. 카드가 뭔가 위에 놓인 물건처럼 안 보일까 걱정했는데, 카드는 원래 그림자를 두 개 달고 있어서 그 일은 계속 그림자가 하고 있었습니다.
창을 넓게 열면 카드 게임이 아니게 되는 것도 이때 고쳤어요. 보드의 모든 치수는 주어진 폭에서 나오는데 폰에는 제한을 둘 만한 폭이 없었거든요. 1920px에서는 일곱 장이 화면을 가로질러 일렬로 늘어서서, 한눈에 읽는 손패가 아니게 됩니다.
여는 화면도 손봤어요. 마크가 카드 두 장이라 카드가 나타나면서 동시에 갈라졌는데, 그러면 이 게임이 무슨 이야기인지 보여줄 첫 장면이 그냥 지나가 버립니다. 한 장이 260ms 동안 놓이고, 180ms 아무 일도 안 일어나고, 그다음에 두 번째가 갈라져 나오게 했어요. 그 멈춤이 애니메이션에 얹은 장식이 아니라 첫 상태를 읽히게 하는 유일한 장치입니다. 이름은 앱에서 유일하게 Pretendard가 아닌 글자예요. UI 서체로 쓰면 "Dripple이라고 적힌 제목"으로 읽히지 로고로 안 읽히더라고요.
처음엔 진짜 게임 위에 코치 마크를 띄우려고 했어요. 그런데 손패는 무작위로 나오니까, "문장을 만들어 보세요"라고 시켰는데 그 손에 만들 수 있는 문장이 없을 수 있습니다. 그러면 배우는 사람이 얻는 건 "이 게임 고장났네"뿐이에요.
그래서 배우기를 아예 다른 모드로 뺐습니다. the cat runs가 나오게 짜인 손패와, 덱 맨 위에 big을 올려둔 1인용 판을 따로 돌려요. 모든 지시를 실제로 따를 수 있게요. 코치 마크는 가리킬 게 있을 때만 화면을 덮고, 손을 요구하는 단계에서는 목표에 테두리만 두르고 아무것도 안 가립니다.
안내문을 담던 종이도 나중에 걷어냈어요. 종이는 카드가 서는 재료인데 안내문까지 종이에 얹으니 화면에 카드가 한 장 더 있는 것처럼 보였습니다. 지금은 글자만 바닥에 바로 얹혀요. 몇 번째 걸음인지 세는 숫자도 같이 올렸습니다.
온라인을 붙이려는데, Realtime DB는 저장은 해도 코드를 돌리진 않습니다. 그러면 셔플을 누군가의 폰에서 해야 하는데, 한 사람이 덱과 남의 손패를 다 쥐고 있는 건 카드 게임이 아니죠.
그래서 서버를 세웠어요. Dart로요. 문법 엔진을 두 번 구현하면 언젠가 둘이 어긋나는 날이 오고 그날은 못 고칩니다. 그래서 규칙을 dripple_rules 패키지로 뽑아 앱과 서버가 같은 코드를 보게 했어요.
내보내는 모양도 둘로 갈랐습니다. 서버가 들고 있는 진짜 상태와, 테이블에서 보여도 되는 공개 뷰요. 남의 손패가 새지 않는다는 걸 테스트로 못 박아 뒀습니다. 대신 문장을 짜고 손패 순서를 바꾸는 건 전부 기기에서 해요. 그건 남이 볼 수 없는 일이라 보낼 이유가 없고, 완성된 문장만 서버로 갑니다.