├── requirements.txt ├── .gitignore ├── images └── game-dev-story-tool.png ├── README.md ├── raw ├── best.csv ├── unique.csv └── not_bad.csv ├── data.py └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5==5.15.7 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | .idea 3 | dist 4 | build 5 | *.spec 6 | 7 | # Python 8 | *.pyc -------------------------------------------------------------------------------- /images/game-dev-story-tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndersonJo/game-dev-story-combo-tool/master/images/game-dev-story-tool.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 게임 개발 스토리 조합 툴 2 | 3 | 현재 보유하고 있는 장르, 내용(게임) 을 선택하면 최적으로 만들수 있는 게임을 알려줍니다. 4 | 5 | ![Game Dev Tool](images/game-dev-story-tool.png) 6 | 7 | ## 실행방법 8 | 9 | 실행전 다음의 library 가 필요합니다. 10 | 11 | - Python 3.x 이상 12 | - PyQT5 13 | 14 | 툴 실행은 다음과 같이 합니다. 15 | 16 | ```bash 17 | $ python main.py 18 | ``` 19 | 20 | -------------------------------------------------------------------------------- /raw/best.csv: -------------------------------------------------------------------------------- 1 | RPG,"판타지[1], 여성향, 송이버섯" 2 | 시뮬레이션,"말 레이스, 연애, 전차, 헌책방, 만화가, 영화, 게임 회사, 마을 개발, 빌딩 건축, 미소녀, 애니메이션, 아이돌, 송이 버섯, 모터 스포츠, 축구, F1[2]" 3 | 테이블 게임,"판초, 사천성" 4 | 액션게임,"닌자, 호러, 시대극, 여성향, 농구, 씨름" 5 | 어드벤처,"탐정, 미스터리, 중세 말기, 애니메이션" 6 | 슈팅,"로봇, 고리 던지기" 7 | 액션 RPG,"사냥, 판초" 8 | 레이싱 게임,"스노우 보드, 모터 스포츠" 9 | 온라인 RPG,중세 유럽 10 | 온라인 SLG,"편의점, 게임 회사, 빌딩 건축, 주식, 아이돌, 송이 버섯" 11 | 수집,"코미디, 코스프레, 미니스커트, 씨름" 12 | 육성,"뇌 트레이닝, 동물, 만화가, 게임 회사, 마을 개발, 아이돌, 송이 버섯, 프로레슬링, 축구, 탁구, F1, 모에" 13 | 보드게임,장기 14 | 퍼즐,"바둑, 오셀로[3]" 15 | 리듬게임,"댄스, 드럼" 16 | 사운드 노벨,"연애, 호러, 영화, 미소녀, 탐정, 중세 말기, 모에" 17 | 체감 게임,"아이돌, 댄스, 드럼, 피트니스, 스키, 스노우 보드, 배구, 핀볼, 슬롯" -------------------------------------------------------------------------------- /raw/unique.csv: -------------------------------------------------------------------------------- 1 | RPG,"호러, 전차, 미술, 빌딩 건축, 코스프레, 댄스, 골프, 씨름, F1" 2 | 시뮬레이션,"스포츠, 미니 스커트, 수영" 3 | 테이블 게임,"무술, 미남, 레슬링" 4 | 액션 게임,"코미디, 전차, 재판, 탐정, 댄스, 골프, 핀볼, 모에" 5 | 어드벤쳐,"무술, 코미디, 전차, 닌자, 고리 던지기, 코스프레, 미니 스커트, 탁구" 6 | 슈팅,"무술, 판타지, 말 레이스, 시대극, 재판, 사냥, 송이 버섯, 사천성, 프로레슬링, 배구, F1" 7 | 액션 RPG,"댄스, 농구" 8 | 레이싱 게임,"무사, 던전, 전차, 게임 회사, 요괴, 미소녀, 여성향, 중세 말기, 대통령, 코스프레, 미니 스커트, 송이 버섯, 바둑, 피트니스, 야구, 모에" 9 | 온라인 RPG,"모험물, 수영복, 판쵸, 오셀로" 10 | 수집,"무술, 연애, 보물찾기, 수영복" 11 | 육성,"판타지, 미술" 12 | 보드 게임,"고리 던지기, 스노우 보드" 13 | 퍼즐,"스포츠, 판타지, 미술, 이집트, 미남" 14 | 리듬 게임,"편의점, 미소녀, 탐정, 중세 말기, 수영복, 바둑, 사천성, 농구, 탁구, 레슬링, F1, 모에" 15 | 사운드 노벨,"무술, 동물, 마을 개발, 미니 스커트, 수영, 야구" 16 | 체감 게임,"스포츠, 말 레이스, 연애, 코미디, 뇌 트레이닝, 헌책방, 빌딩 건축, 요괴, 미소녀, 여성향, 미스터리, 고리 던지기, 주식, 애니메이션, 코스프레, 수영복, 프로레슬링, 레슬링, 모에" -------------------------------------------------------------------------------- /raw/not_bad.csv: -------------------------------------------------------------------------------- 1 | RPG,"모험물, 던전, 이집트, 중세 유럽, 미스터리, 중세 말기, 애니메이션" 2 | 시뮬레이션,"무사, 보물찾기, 코미디, 뇌 트레이닝, 동물, 비행기, 편의점, 시대극, 이집트, 재판, 사냥, 고등학교, 중학교, 중세 말기, 대통령, 주식, 사천성, 배구, 야구, 핀볼, 슬롯, 모에" 3 | 테이블 게임,"판타지, 던전, 뇌 트레이닝, 중세 유럽, 전쟁, 장기, 바둑, 오셀로, 핀볼, 슬롯" 4 | 액션 게임,"스포츠, 모험물, 무술, 말 레이스, 무사, 던전, 로봇, 비행기, 고등학교, 중학교, 중세 말기, 애니메이션, 드럼, 스키, 수영, 배구, 모터 스포츠, 축구, 탁구, 야구, 레슬링, 마라톤, F1" 5 | 어드벤처,"판타지, 연애, 호러, 미술, 이집트, 중세 유럽, 미소녀, 씨름, 모에" 6 | 슈팅,"던전, 비행기, 요괴, 전쟁, 닌자, 미남, 골프" 7 | 액션 RPG,"모험물, 판타지, 중세 유럽, 중세 말기, 애니메이션" 8 | 레이싱 게임,"스포츠, 말 레이스, 비행기, 스키, 수영, 마라톤, F1" 9 | 온라인 RPG,"판타지, 던전, 시대극, 사냥" 10 | 온라인 SLG,"무사, 시대극, 헌책방, 전쟁, 모터 스포츠, 레슬링" 11 | 수집,"전차, 동물, 로봇, 만화가, 영화, 이집트, 재판, 요괴, 닌자, 미소녀, 중세 말기, 애니메이션, 아이돌, 송이 버섯, 장기 ,피트니스, 스노우 보드, 골프, 축구, F1, 모에" 12 | 육성,"스포츠, 코미디, 편의점, 영화, 미소녀, 탐정, 대통령, 코스프레, 댄스, 농구, 수영, 모터 스포츠, 씨름, 야구, 레슬링" 13 | 보드 게임,"무사, 던전, 전차, 중세 유럽, 애니메이션, 바둑, 사천성, 오셀로, 탁구, 야구, 슬롯" 14 | 퍼즐,"뇌 트레이닝, 장기, 사천성, 슬롯" 15 | 리듬 게임,"영화, 아이돌, 스노우 보드" 16 | 사운드 노벨,"시대극, 고등학교, 중학교, 전쟁, 미남, 미스터리, 아이돌" -------------------------------------------------------------------------------- /data.py: -------------------------------------------------------------------------------- 1 | import csv 2 | from pathlib import Path 3 | from typing import Dict 4 | 5 | FILE_SCORES = [ 6 | ('raw/best.csv', 4), 7 | ('raw/unique.csv', 2), 8 | ('raw/not_bad.csv', 1), 9 | ] 10 | 11 | TRANSLATIONS = { 12 | '어드벤쳐': '어드벤처' 13 | } 14 | 15 | SCORE_LABELS = { 16 | 4: '걸작', 17 | 2: '독창적', 18 | 1: '나쁘지 않음' 19 | } 20 | 21 | 22 | def generate_data() -> Dict[str, Dict[str, int]]: 23 | def preprocess_games(word, score) -> dict: 24 | word = list(word) 25 | start = None 26 | for i, c in enumerate(word): 27 | if c == '[': 28 | start = i 29 | elif c == ']': 30 | word[start:i + 1] = [''] 31 | word = ''.join(word) 32 | return {game.strip(): score for game in word.split(',')} 33 | 34 | data = dict() 35 | for file_path, score in FILE_SCORES: 36 | file_path = Path(file_path) 37 | 38 | with open(file_path, 'r', encoding='utf-8') as f: 39 | reader = csv.reader(f) 40 | for col in reader: 41 | genre = col[0] 42 | genre = genre.replace('게임', '').strip() 43 | genre = TRANSLATIONS.get(genre, genre) 44 | data.setdefault(genre, dict()) 45 | 46 | games = preprocess_games(col[1], score) 47 | data[genre].update(games) 48 | return data 49 | 50 | 51 | if __name__ == '__main__': 52 | generate_data() 53 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import sys 3 | import bisect 4 | from pathlib import Path 5 | from tempfile import gettempdir 6 | from typing import List, Optional, Tuple 7 | 8 | from PyQt5.QtWidgets import QApplication, QLabel, QWidget, QGridLayout, QCheckBox, QScrollArea, QPushButton, QMessageBox 9 | from PyQt5.QtCore import Qt, pyqtSignal, QObject, pyqtSlot 10 | 11 | from data import generate_data, SCORE_LABELS 12 | 13 | 14 | class GameDev(QWidget): 15 | def __init__(self): 16 | super().__init__() 17 | 18 | # Data 19 | self.data = generate_data() 20 | self.unique_genres: List[str] = sorted(list(self.data)) 21 | unique_games = set() 22 | for v in self.data.values(): 23 | unique_games |= set(v) 24 | self.unique_games: List[str] = sorted(list(unique_games)) 25 | 26 | # Init QT5 27 | self._init_app() 28 | 29 | def _init_app(self): 30 | def make_subtitle(title) -> QLabel: 31 | q_label = QLabel(title) 32 | q_label.setStyleSheet('font-size:20pt;' 33 | 'font-weight:bold;') 34 | return q_label 35 | 36 | self.setWindowTitle('게임 개발 스토리 조합툴') 37 | self.main = QGridLayout() 38 | self.setMinimumWidth(700) 39 | self.setLayout(self.main) 40 | 41 | # Title 42 | self.main.addWidget(make_subtitle('장르'), 0, 0) 43 | self.main.addWidget(make_subtitle('게임'), 0, 1) 44 | self.main.addWidget(make_subtitle('결과'), 0, 2) 45 | 46 | # Selection Options 47 | genre_signal = SelectWidget.CheckBoxSignal() 48 | game_signal = SelectWidget.CheckBoxSignal() 49 | 50 | genre_signal.signal.connect(self.on_checkbox_changed) 51 | game_signal.signal.connect(self.on_checkbox_changed) 52 | self.genre_widget = SelectWidget(self.unique_genres, genre_signal) 53 | self.game_widget = SelectWidget(self.unique_games, game_signal) 54 | 55 | game_scroll = QScrollArea() 56 | game_scroll.setWidgetResizable(True) 57 | game_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 58 | game_scroll.setWidget(self.game_widget) 59 | 60 | self.main.addWidget(self.genre_widget, 1, 0) 61 | self.main.addWidget(game_scroll, 1, 1) 62 | 63 | # Output 64 | self.output_widget = OutputWidget() 65 | output_scroll = QScrollArea() 66 | output_scroll.setWidgetResizable(True) 67 | output_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 68 | output_scroll.setWidget(self.output_widget) 69 | self.main.addWidget(output_scroll, 1, 2) 70 | 71 | # Buttons 72 | save_button = QPushButton('Save') 73 | save_button.pressed.connect(self.on_save) 74 | reset_button = QPushButton('Reset') 75 | reset_button.pressed.connect(self.on_reset) 76 | self.main.addWidget(save_button, 2, 1) 77 | self.main.addWidget(reset_button, 2, 2) 78 | 79 | # Load 80 | saved_path = Path(gettempdir()) / 'game-dev.pkl' 81 | if saved_path.exists(): 82 | with open(saved_path, 'rb') as f: 83 | data = pickle.load(f) 84 | for i, checked in enumerate(data['genre']): 85 | self.genre_widget.grid.itemAt(i).widget().setChecked(checked) 86 | 87 | for i, checked in enumerate(data['game']): 88 | self.game_widget.grid.itemAt(i).widget().setChecked(checked) 89 | 90 | @pyqtSlot() 91 | def on_checkbox_changed(self, ): 92 | genre_checks = self.genre_widget.list_checked() 93 | game_checks = self.game_widget.list_checked() 94 | 95 | checked_genres = [v for c, v in zip(genre_checks, self.unique_genres) if c] 96 | checked_games = [v for c, v in zip(game_checks, self.unique_games) if c] 97 | 98 | output_data = [] 99 | for genre in checked_genres: 100 | for game in checked_games: 101 | if genre in self.data and game in self.data[genre]: 102 | output_data.append((self.data[genre][game], genre, game)) 103 | self.output_widget.set_data(output_data) 104 | 105 | @pyqtSlot() 106 | def on_save(self): 107 | genre_checks = self.genre_widget.list_checked() 108 | game_checks = self.game_widget.list_checked() 109 | 110 | with open(Path(gettempdir()) / 'game-dev.pkl', 'wb') as f: 111 | pickle.dump({'genre': genre_checks, 112 | 'game': game_checks}, f) 113 | 114 | msgBox = QMessageBox() 115 | msgBox.setText('saved') 116 | msgBox.setWindowTitle('Saved') 117 | msgBox.show() 118 | 119 | @pyqtSlot() 120 | def on_reset(self): 121 | self.game_widget.clear_checkboxes() 122 | self.genre_widget.clear_checkboxes() 123 | 124 | 125 | class SelectWidget(QWidget): 126 | class CheckBoxSignal(QObject): 127 | signal = pyqtSignal() 128 | 129 | def __init__(self, list_data: List[str], signal: CheckBoxSignal): 130 | super().__init__() 131 | self.list_data = list_data 132 | self.signal = signal 133 | self.grid = QGridLayout() 134 | self.setLayout(self.grid) 135 | self.grid.setAlignment(Qt.AlignTop) 136 | self.grid.setSpacing(0) 137 | self.setMinimumWidth(150) 138 | 139 | self.checkboxes: List[Optional[QCheckBox]] = [None] * len(self.list_data) 140 | for i, genre in enumerate(self.list_data): 141 | self.checkboxes[i] = QCheckBox(genre) 142 | self.checkboxes[i].stateChanged.connect(self.listen_checkbox_pressed) 143 | self.grid.addWidget(self.checkboxes[i], i, 0) 144 | 145 | def clear_checkboxes(self): 146 | for i in reversed(range(self.grid.count())): 147 | self.grid.itemAt(i).widget().setChecked(False) 148 | 149 | def listen_checkbox_pressed(self, state): 150 | self.signal.signal.emit() 151 | 152 | def list_checked(self) -> List[bool]: 153 | return [c.isChecked() for c in self.checkboxes] 154 | 155 | 156 | class OutputWidget(QWidget): 157 | def __init__(self): 158 | super().__init__() 159 | self.data = [] 160 | self.grid = QGridLayout() 161 | self.setLayout(self.grid) 162 | self.grid.setAlignment(Qt.AlignTop) 163 | 164 | def set_data(self, output_data: List[Tuple[int, str, str]]): 165 | def make_labels(_score, _genre, _game) -> Tuple[QLabel, QLabel, QLabel]: 166 | _score_label = QLabel(SCORE_LABELS[_score]) 167 | _score_label.setStyleSheet('font-weight:bold') 168 | _genre_label = QLabel(_genre) 169 | _game_label = QLabel(_game) 170 | return _score_label, _genre_label, _game_label 171 | 172 | # Clear 173 | self.data.clear() 174 | for i in reversed(range(self.grid.count())): 175 | self.grid.itemAt(i).widget().setParent(None) 176 | 177 | # Sorted by Score 178 | self.data = sorted(output_data, key=lambda x: -x[0]) 179 | 180 | # Add Items 181 | for i, (data_type, genre, game) in enumerate(self.data): 182 | score_label, genre_label, game_label = make_labels(data_type, genre, game) 183 | self.grid.addWidget(score_label, i, 0) 184 | self.grid.addWidget(genre_label, i, 1) 185 | self.grid.addWidget(game_label, i, 2) 186 | 187 | 188 | def main(): 189 | app = QApplication(sys.argv) 190 | game = GameDev() 191 | game.show() 192 | sys.exit(app.exec_()) 193 | 194 | 195 | if __name__ == '__main__': 196 | main() 197 | --------------------------------------------------------------------------------