grammar for inspirations and developers lists

This commit is contained in:
Trilarion 2020-08-31 23:50:45 +02:00
parent 0d208dbdf7
commit 881f0c77c5
7 changed files with 154 additions and 93 deletions

View File

@ -5,14 +5,21 @@ http://cdetect.sourceforge.net/
http://circularstudios.com/ http://circularstudios.com/
http://cyxdown.free.fr/bs/ http://cyxdown.free.fr/bs/
http://cyxdown.free.fr/f2b/ http://cyxdown.free.fr/f2b/
https://github.com/nfprojects/nfengine
http://dead-code.org/home/ http://dead-code.org/home/
http://e-adventure.e-ucm.es/login/index.php (games of eAdventure) http://e-adventure.e-ucm.es/login/index.php (games of eAdventure)
http://ethernet.wasted.ch/ http://ethernet.wasted.ch/
http://evolonline.org/about http://evolonline.org/about
http://game-editor.com/Main_Page http://game-editor.com/Main_Page
http://giderosmobile.com/ http://giderosmobile.com/
https://github.com/skylicht-lab/skylicht-engine
https://github.com/etlegacy/etlegacy
https://github.com/Soldat/soldat
https://github.com/guillaumechereau/goxel
http://haxepunk.com/ http://haxepunk.com/
http://hcsoftware.sourceforge.net/jason-rohrer/ (various games there) http://hcsoftware.sourceforge.net/jason-rohrer/ (various games there)
https://github.com/cxong/cdogs-sdl
https://github.com/terrafx/terrafx
http://hgm.nubati.net/ http://hgm.nubati.net/
http://icculus.org/ http://icculus.org/
http://icculus.org/asciiroth/ http://icculus.org/asciiroth/

View File

@ -1,17 +1,18 @@
start: header entry* start: entry*
entry: "##" name "(" _NUMBER ")\n" property+
property: "-" _key ":" _values "\n"
_key: /(?! ).+?(?=:)(?<! )/ // key: everything until next ":", not beginning or ending with a space
_values: [_value ("," _value)*]
_value: quoted_value | unquoted_value
quoted_value: /\".+?\"/ // with quotation marks, can contain commas
unquoted_value: /(?![ \"])[^,\n]+(?<![ \"])/ // cannot contain commas, cannot start or end with quotation mark
header: "# " name " (" number ")\n" _E name: /(?! ).+?(?= \()/ // developer name: everything until " ("
entry: "## " name " (" number ")\n" _E property+ _E _NUMBER: /[0-9]+/
property: "- " _key ": " _value "\n" %import common.WS
_key: /(?! ).+?(?=:)(?<! )/ // key: everything until next ":", not beginning or ending with a space %ignore WS
_value: /.+(?<! )/ // everything until the end of the line, not ending with a space %ignore /^\[comment\]: #.*$\n/m // [comment]: # xxx
%ignore /^# .+$\n/m // the line starting with "# "
name: /.+?(?= \()/ // developer name: everything until " (" %ignore /^$\n/m // empty lines
number: /[0-9]+/
COMMENT: /^\[comment\]: #.*$\n/m // [comment]: # xxx
_E: /^$\n/m // empty new line
%ignore COMMENT

View File

@ -14,6 +14,8 @@ import json
import textwrap import textwrap
import os import os
import re import re
import utils.constants
from utils import constants as c, utils, osg from utils import constants as c, utils, osg
@ -76,7 +78,7 @@ def update_readme_and_tocs(infos):
# create by category # create by category
categories_text = [] categories_text = []
for keyword in osg.recommended_keywords: for keyword in utils.constants.recommended_keywords:
infos_filtered = [x for x in infos if keyword in x['keywords']] infos_filtered = [x for x in infos if keyword in x['keywords']]
title = keyword.capitalize() title = keyword.capitalize()
name = keyword.replace(' ', '-') name = keyword.replace(' ', '-')
@ -88,7 +90,7 @@ def update_readme_and_tocs(infos):
# create by platform # create by platform
platforms_text = [] platforms_text = []
for platform in osg.valid_platforms: for platform in utils.constants.valid_platforms:
infos_filtered = [x for x in infos if platform in x.get('platform', [])] infos_filtered = [x for x in infos if platform in x.get('platform', [])]
title = platform title = platform
name = platform.lower() name = platform.lower()
@ -271,7 +273,7 @@ def fix_entries():
elements = list(set(elements)) elements = list(set(elements))
# get category out # get category out
for keyword in osg.recommended_keywords: for keyword in utils.constants.recommended_keywords:
if keyword in elements: if keyword in elements:
elements.remove(keyword) elements.remove(keyword)
category = keyword category = keyword
@ -949,12 +951,12 @@ def check_code_dependencies(infos):
""" """
# get all names of frameworks and library also using osg.code_dependencies_aliases # get all names of frameworks and library also using osg.code_dependencies_aliases
valid_dependencies = list(osg.code_dependencies_without_entry.keys()) valid_dependencies = list(utils.constants.general_code_dependencies_without_entry.keys())
for info in infos: for info in infos:
if any((x in ('framework', 'library', 'game engine') for x in info['keywords'])): if any((x in ('framework', 'library', 'game engine') for x in info['keywords'])):
name = info['name'] name = info['name']
if name in osg.code_dependencies_aliases: if name in utils.constants.code_dependencies_aliases:
valid_dependencies.extend(osg.code_dependencies_aliases[name]) valid_dependencies.extend(utils.constants.code_dependencies_aliases[name])
else: else:
valid_dependencies.append(name) valid_dependencies.append(name)

View File

@ -6,4 +6,4 @@ if __name__ == "__main__":
osg.write_inspirations_info(inspirations) # write again just to check integrity osg.write_inspirations_info(inspirations) # write again just to check integrity
# assemble info # assemble info
entries = osg.assemble_infos() # entries = osg.assemble_infos()

View File

@ -11,6 +11,10 @@ entries_path = os.path.join(root_path, 'entries')
tocs_path = os.path.join(entries_path, 'tocs') tocs_path = os.path.join(entries_path, 'tocs')
code_path = os.path.join(root_path, 'code') code_path = os.path.join(root_path, 'code')
inspirations_file = os.path.join(root_path, 'inspirations.md')
developer_file = os.path.join(root_path, 'developer.md')
# local config
local_config_file = os.path.join(root_path, 'local-config.ini') local_config_file = os.path.join(root_path, 'local-config.ini')
config = configparser.ConfigParser() config = configparser.ConfigParser()
@ -24,3 +28,68 @@ def get_config(key):
:return: :return:
""" """
return config['general'][key] return config['general'][key]
# database entry constants
generic_comment_string = '[comment]: # (partly autogenerated content, edit with care, read the manual before)'
# these fields have to be present in each entry (in this order)
essential_fields = ('Home', 'State', 'Keywords', 'Code repository', 'Code language', 'Code license')
# only these fields can be used currently (in this order)
valid_fields = (
'Home', 'Media', 'State', 'Play', 'Download', 'Platform', 'Keywords', 'Code repository', 'Code language',
'Code license', 'Code dependencies', 'Assets license', 'Developer', 'Build system', 'Build instructions')
# these are the only valid platforms currently (and must be given in this order)
valid_platforms = ('Windows', 'Linux', 'macOS', 'Android', 'iOS', 'Web')
# at least one of these must be used for every entry, this gives the principal categories and the order of the categories
recommended_keywords = (
'action', 'arcade', 'adventure', 'visual novel', 'sports', 'platform', 'puzzle', 'role playing', 'simulation',
'strategy', 'cards', 'board', 'music', 'educational', 'tool', 'game engine', 'framework', 'library', 'remake')
# known programming languages, anything else will result in a warning during a maintenance operation
# only these will be used when gathering statistics
known_languages = (
'AGS Script', 'ActionScript', 'Ada', 'AngelScript', 'Assembly', 'Basic', 'Blender Script', 'BlitzMax', 'C', 'C#',
'C++', 'Clojure', 'CoffeeScript', 'ColdFusion', 'D', 'DM', 'Dart', 'Dia', 'Elm', 'Emacs Lisp', 'F#', 'GDScript',
'Game Maker Script', 'Go', 'Groovy', 'Haskell', 'Haxe', 'Io', 'Java', 'JavaScript', 'Kotlin', 'Lisp', 'Lua',
'MegaGlest Script', 'MoonScript', 'None', 'OCaml', 'Objective-C', 'PHP', 'Pascal', 'Perl', 'Python', 'QuakeC', 'R',
"Ren'py", 'Ruby', 'Rust', 'Scala', 'Scheme', 'Script', 'Shell', 'Swift', 'TorqueScript', 'TypeScript', 'Vala',
'Visual Basic', 'XUL', 'ZenScript', 'ooc')
# known licenses, anything outside of this will result in a warning during a maintenance operation
# only these will be used when gathering statistics
known_licenses = (
'2-clause BSD', '3-clause BSD', 'AFL-3.0', 'AGPL-3.0', 'Apache-2.0', 'Artistic License-1.0', 'Artistic License-2.0',
'Boost-1.0', 'CC-BY-NC-3.0', 'CC-BY-NC-SA-2.0', 'CC-BY-NC-SA-3.0', 'CC-BY-SA-3.0', 'CC-BY-NC-SA-4.0',
'CC-BY-SA-4.0', 'CC0', 'Custom', 'EPL-2.0', 'GPL-2.0', 'GPL-3.0', 'IJG', 'ISC', 'Java Research License', 'LGPL-2.0',
'LGPL-2.1', 'LGPL-3.0', 'MAME', 'MIT', 'MPL-1.1', 'MPL-2.0', 'MS-PL', 'MS-RL', 'NetHack General Public License',
'None', 'Proprietary', 'Public domain', 'SWIG license', 'Unlicense', 'WTFPL', 'wxWindows license', 'zlib')
# valid multiplayer modes (can be combined with "+" )
valid_multiplayer_modes = (
'competitive', 'co-op', 'hotseat', 'LAN', 'local', 'massive', 'matchmaking', 'online', 'split-screen')
# TODO put the abbreviations directly in the name line (parenthesis maybe), that is more natural
# this is a mapping of entry name to abbreviation and the abbreviations are used when specifying code dependencies
code_dependencies_aliases = {'Simple DirectMedia Layer': ('SDL', 'SDL2'), 'Simple and Fast Multimedia Library': ('SFML',),
'Boost (C++ Libraries)': ('Boost',), 'SGE Game Engine': ('SGE',), 'MegaGlest': ('MegaGlest Engine',)}
# these are code dependencies that won't get their own entry, because they are not centered on gaming
general_code_dependencies_without_entry = {'OpenGL': 'https://www.opengl.org/',
'GLUT': 'https://www.opengl.org/resources/libraries/',
'WebGL': 'https://www.khronos.org/webgl/',
'Unity': 'https://unity.com/solutions/game',
'.NET': 'https://dotnet.microsoft.com/', 'Vulkan': 'https://www.khronos.org/vulkan/',
'KDE Frameworks': 'https://kde.org/products/frameworks/',
'jQuery': 'https://jquery.com/',
'node.js': 'https://nodejs.org/en/',
'GNU Guile': 'https://www.gnu.org/software/guile/',
'tkinter': 'https://docs.python.org/3/library/tk.html'}
# developer information (in the file all fields will be capitalized)
valid_developer_fields = ('name', 'games', 'contact', 'organization', 'home')
# inspiration/original game information (in the file all fields will be capitalized)
valid_inspiration_fields = ('name', 'inspired entries')

View File

@ -3,32 +3,54 @@ Specific functions working on the games.
""" """
import re import re
import os
from difflib import SequenceMatcher from difflib import SequenceMatcher
from utils import utils, constants as c from utils import utils
import lark import lark
from utils.constants import *
class ListingTransformer(lark.Transformer): class ListingTransformer(lark.Transformer):
"""
Transforms content parsed by grammar_listing.lark further.
Used for the developer and inspirations list.
"""
def number(self, x): def unquoted_value(self, x):
raise lark.Discard return x[0].value
def quoted_value(self, x):
return x[0].value[1:-1] # remove quotation marks
def property(self, x): def property(self, x):
return x[0].value.lower(), x[1].value """
The key of a property will be converted to lower case and the value part is the second part
:param x:
:return:
"""
return x[0].lower(), x[1:]
def name(self, x): def name(self, x):
"""
The name part is treated as a property with key "name"
:param x:
:return:
"""
return 'name', x[0].value return 'name', x[0].value
def entry(self, x): def entry(self, x):
"""
All (key, value) tuples are inserted into a dictionary.
:param x:
:return:
"""
d = {} d = {}
for key, value in x: for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value d[key] = value
return d return d
def header(self, x):
raise lark.Discard
def start(self, x): def start(self, x):
return x return x
@ -61,53 +83,9 @@ class EntryTransformer(lark.Transformer):
return 'building', d return 'building', d
essential_fields = ('Home', 'State', 'Keywords', 'Code repository', 'Code language', 'Code license')
valid_fields = (
'Home', 'Media', 'State', 'Play', 'Download', 'Platform', 'Keywords', 'Code repository', 'Code language',
'Code license', 'Code dependencies', 'Assets license', 'Developer', 'Build system', 'Build instructions')
valid_platforms = ('Windows', 'Linux', 'macOS', 'Android', 'iOS', 'Web')
recommended_keywords = (
'action', 'arcade', 'adventure', 'visual novel', 'sports', 'platform', 'puzzle', 'role playing', 'simulation',
'strategy', 'cards', 'board', 'music', 'educational', 'tool', 'game engine', 'framework', 'library', 'remake')
known_languages = (
'AGS Script', 'ActionScript', 'Ada', 'AngelScript', 'Assembly', 'Basic', 'Blender Script', 'BlitzMax', 'C', 'C#',
'C++', 'Clojure', 'CoffeeScript', 'ColdFusion', 'D', 'DM', 'Dart', 'Dia', 'Elm', 'Emacs Lisp', 'F#', 'GDScript',
'Game Maker Script', 'Go', 'Groovy', 'Haskell', 'Haxe', 'Io', 'Java', 'JavaScript', 'Kotlin', 'Lisp', 'Lua',
'MegaGlest Script', 'MoonScript', 'None', 'OCaml', 'Objective-C', 'PHP', 'Pascal', 'Perl', 'Python', 'QuakeC', 'R',
"Ren'py", 'Ruby', 'Rust', 'Scala', 'Scheme', 'Script', 'Shell', 'Swift', 'TorqueScript', 'TypeScript', 'Vala',
'Visual Basic', 'XUL', 'ZenScript', 'ooc')
known_licenses = (
'2-clause BSD', '3-clause BSD', 'AFL-3.0', 'AGPL-3.0', 'Apache-2.0', 'Artistic License-1.0', 'Artistic License-2.0',
'Boost-1.0', 'CC-BY-NC-3.0', 'CC-BY-NC-SA-2.0', 'CC-BY-NC-SA-3.0', 'CC-BY-SA-3.0', 'CC-BY-NC-SA-4.0',
'CC-BY-SA-4.0',
'CC0', 'Custom', 'EPL-2.0', 'GPL-2.0', 'GPL-3.0', 'IJG', 'ISC', 'Java Research License', 'LGPL-2.0', 'LGPL-2.1',
'LGPL-3.0', 'MAME', 'MIT', 'MPL-1.1', 'MPL-2.0', 'MS-PL', 'MS-RL', 'NetHack General Public License', 'None',
'Proprietary', 'Public domain', 'SWIG license', 'Unlicense', 'WTFPL', 'wxWindows license', 'zlib')
known_multiplayer_modes = (
'competitive', 'co-op', 'hotseat', 'LAN', 'local', 'massive', 'matchmaking', 'online', 'split-screen')
# TODO put the abbreviations directly in the name line (parenthesis maybe), that is more natural
code_dependencies_aliases = {'Simple DirectMedia Layer': ('SDL', 'SDL2'), 'Simple and Fast Multimedia Library': ('SFML',),
'Boost (C++ Libraries)': ('Boost',), 'SGE Game Engine': ('SGE',), 'MegaGlest': ('MegaGlest Engine',)}
code_dependencies_without_entry = {'OpenGL': 'https://www.opengl.org/',
'GLUT': 'https://www.opengl.org/resources/libraries/',
'WebGL': 'https://www.khronos.org/webgl/',
'Unity': 'https://unity.com/solutions/game',
'.NET': 'https://dotnet.microsoft.com/', 'Vulkan': 'https://www.khronos.org/vulkan/',
'KDE Frameworks': 'https://kde.org/products/frameworks/',
'jQuery': 'https://jquery.com/',
'node.js': 'https://nodejs.org/en/',
'GNU Guile': 'https://www.gnu.org/software/guile/',
'tkinter': 'https://docs.python.org/3/library/tk.html'}
regex_sanitize_name = re.compile(r"[^A-Za-z 0-9-+]+") regex_sanitize_name = re.compile(r"[^A-Za-z 0-9-+]+")
regex_sanitize_name_space_eater = re.compile(r" +") regex_sanitize_name_space_eater = re.compile(r" +")
valid_developer_fields = ('name', 'games', 'contact', 'organization', 'home')
valid_inspiration_fields = ('name', 'inspired entries')
comment_string = '[comment]: # (partly autogenerated content, edit with care, read the manual before)'
def name_similarity(a, b): def name_similarity(a, b):
return SequenceMatcher(None, str.casefold(a), str.casefold(b)).ratio() return SequenceMatcher(None, str.casefold(a), str.casefold(b)).ratio()
@ -130,11 +108,11 @@ def entry_iterator():
""" """
# get all entries (ignore everything starting with underscore) # get all entries (ignore everything starting with underscore)
entries = os.listdir(c.entries_path) entries = os.listdir(entries_path)
# iterate over all entries # iterate over all entries
for entry in entries: for entry in entries:
entry_path = os.path.join(c.entries_path, entry) entry_path = os.path.join(entries_path, entry)
# ignore directories ("tocs" for example) # ignore directories ("tocs" for example)
if os.path.isdir(entry_path): if os.path.isdir(entry_path):
@ -350,8 +328,8 @@ def assemble_infos():
# we also allow -X with X =2..9 as possible extension (because of duplicate canonical file names) # we also allow -X with X =2..9 as possible extension (because of duplicate canonical file names)
if canonical_file_name != entry and canonical_file_name != entry[:-5] + '.md': if canonical_file_name != entry and canonical_file_name != entry[:-5] + '.md':
print('Warning: file {} should be {}'.format(entry, canonical_file_name)) print('Warning: file {} should be {}'.format(entry, canonical_file_name))
source_file = os.path.join(c.entries_path, entry) source_file = os.path.join(entries_path, entry)
target_file = os.path.join(c.entries_path, canonical_file_name) target_file = os.path.join(entries_path, canonical_file_name)
if not os.path.isfile(target_file): if not os.path.isfile(target_file):
pass pass
# os.rename(source_file, target_file) # os.rename(source_file, target_file)
@ -390,9 +368,10 @@ def extract_links():
return urls return urls
def read_and_parse(content_file, grammar_file, transformer): def read_and_parse(content_file: str, grammar_file: str, transformer: lark.Transformer):
""" """
Reads a content file and a grammar file and parses the content with the grammar following by
transforming the parsed output and returning the transformed result.
:param content_file: :param content_file:
:param grammar_file: :param grammar_file:
:param transformer: :param transformer:
@ -410,8 +389,7 @@ def read_developer_info():
:return: :return:
""" """
developer_file = os.path.join(c.root_path, 'developer.md') grammar_file = os.path.join(code_path, 'grammar_listing.lark')
grammar_file = os.path.join(c.code_path, 'grammar_listing.lark')
transformer = ListingTransformer() transformer = ListingTransformer()
developers = read_and_parse(developer_file, grammar_file, transformer) developers = read_and_parse(developer_file, grammar_file, transformer)
# now transform a bit more # now transform a bit more
@ -446,7 +424,7 @@ def write_developer_info(developers):
:return: :return:
""" """
# comment # comment
content = '{}\n'.format(comment_string) content = '{}\n'.format(generic_comment_string)
# number of developer # number of developer
content += '# Developer ({})\n\n'.format(len(developers)) content += '# Developer ({})\n\n'.format(len(developers))
@ -474,22 +452,26 @@ def write_developer_info(developers):
content += '\n' content += '\n'
# write # write
developer_file = os.path.join(c.root_path, 'developer.md')
utils.write_text(developer_file, content) utils.write_text(developer_file, content)
def read_inspirations_info(): def read_inspirations_info():
""" """
Reads the info list about the games originals/inspirations from inspirations.md using the Lark parser grammar
in grammar_listing.lark
:return: :return:
""" """
inspirations_file = os.path.join(c.root_path, 'inspirations.md') # read inspirations
grammar_file = os.path.join(c.code_path, 'grammar_listing.lark')
grammar_file = os.path.join(code_path, 'grammar_listing.lark')
transformer = ListingTransformer() transformer = ListingTransformer()
inspirations = read_and_parse(inspirations_file, grammar_file, transformer) inspirations = read_and_parse(inspirations_file, grammar_file, transformer)
# now inspirations is a list of dictionaries for every entry with keys (valid_developers_fields)
# now transform a bit more # now transform a bit more
for index, inspiration in enumerate(inspirations): for index, inspiration in enumerate(inspirations):
# check for valid keys # check that keys are valid keys
for field in inspiration.keys(): for field in inspiration.keys():
if field not in valid_inspiration_fields: if field not in valid_inspiration_fields:
raise RuntimeError('Unknown field "{}" for inspiration: {}.'.format(field, inspiration['name'])) raise RuntimeError('Unknown field "{}" for inspiration: {}.'.format(field, inspiration['name']))
@ -497,26 +479,27 @@ def read_inspirations_info():
for field in ('inspired entries',): for field in ('inspired entries',):
if field in inspiration: if field in inspiration:
content = inspiration[field] content = inspiration[field]
content = content.split(',')
content = [x.strip() for x in content] content = [x.strip() for x in content]
inspiration[field] = content inspiration[field] = content
# check for duplicate names entries # check for duplicate names entries
names = [inspiration['name'] for inspiration in inspirations] names = [inspiration['name'] for inspiration in inspirations]
duplicate_names = (name for name in names if names.count(name) > 1) duplicate_names = (name for name in names if names.count(name) > 1)
duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names
if duplicate_names: if duplicate_names:
print('Warning: duplicate inspiration names: {}'.format(', '.join(duplicate_names))) print('Warning: duplicate inspiration names: {}'.format(', '.join(duplicate_names)))
return inspirations return inspirations
def write_inspirations_info(inspirations): def write_inspirations_info(inspirations):
""" """
Given an internal list of inspirations, write it into the inspirations file
:param inspirations: :param inspirations:
:return: :return:
""" """
# comment # comment
content = '{}\n'.format(comment_string) content = '{}\n'.format(generic_comment_string)
# number of developer # number of developer
content += '# Inspirations ({})\n\n'.format(len(inspirations)) content += '# Inspirations ({})\n\n'.format(len(inspirations))
@ -545,7 +528,6 @@ def write_inspirations_info(inspirations):
content += '\n' content += '\n'
# write # write
inspirations_file = os.path.join(c.root_path, 'inspirations2.md')
utils.write_text(inspirations_file, content) utils.write_text(inspirations_file, content)

View File

@ -31,7 +31,7 @@
## Achtung die Kurve! (3) ## Achtung die Kurve! (3)
- Inspired entries: Achtung, die Kurve!, Netacka, Zatacka X - Inspired entries: "Achtung, die Kurve!", Netacka, Zatacka X
## Advance Wars (1) ## Advance Wars (1)
@ -1311,7 +1311,7 @@
## RARS (1) ## RARS (1)
- Inspired entries: TORCS, The Open Racing Car Simulator - Inspired entries: "The Open Racing Car Simulator, TORCS"
## Redneck Rampage (1) ## Redneck Rampage (1)