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://cyxdown.free.fr/bs/
http://cyxdown.free.fr/f2b/
https://github.com/nfprojects/nfengine
http://dead-code.org/home/
http://e-adventure.e-ucm.es/login/index.php (games of eAdventure)
http://ethernet.wasted.ch/
http://evolonline.org/about
http://game-editor.com/Main_Page
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://hcsoftware.sourceforge.net/jason-rohrer/ (various games there)
https://github.com/cxong/cdogs-sdl
https://github.com/terrafx/terrafx
http://hgm.nubati.net/
http://icculus.org/
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"
_key: /(?! ).+?(?=:)(?<! )/ // key: everything until next ":", not beginning or ending with a space
_value: /.+(?<! )/ // everything until the end of the line, not ending with a space
name: /.+?(?= \()/ // developer name: everything until " ("
number: /[0-9]+/
COMMENT: /^\[comment\]: #.*$\n/m // [comment]: # xxx
_E: /^$\n/m // empty new line
%ignore COMMENT
%import common.WS
%ignore WS
%ignore /^\[comment\]: #.*$\n/m // [comment]: # xxx
%ignore /^# .+$\n/m // the line starting with "# "
%ignore /^$\n/m // empty lines

View File

@ -14,6 +14,8 @@ import json
import textwrap
import os
import re
import utils.constants
from utils import constants as c, utils, osg
@ -76,7 +78,7 @@ def update_readme_and_tocs(infos):
# create by category
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']]
title = keyword.capitalize()
name = keyword.replace(' ', '-')
@ -88,7 +90,7 @@ def update_readme_and_tocs(infos):
# create by platform
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', [])]
title = platform
name = platform.lower()
@ -271,7 +273,7 @@ def fix_entries():
elements = list(set(elements))
# get category out
for keyword in osg.recommended_keywords:
for keyword in utils.constants.recommended_keywords:
if keyword in elements:
elements.remove(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
valid_dependencies = list(osg.code_dependencies_without_entry.keys())
valid_dependencies = list(utils.constants.general_code_dependencies_without_entry.keys())
for info in infos:
if any((x in ('framework', 'library', 'game engine') for x in info['keywords'])):
name = info['name']
if name in osg.code_dependencies_aliases:
valid_dependencies.extend(osg.code_dependencies_aliases[name])
if name in utils.constants.code_dependencies_aliases:
valid_dependencies.extend(utils.constants.code_dependencies_aliases[name])
else:
valid_dependencies.append(name)

View File

@ -6,4 +6,4 @@ if __name__ == "__main__":
osg.write_inspirations_info(inspirations) # write again just to check integrity
# 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')
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')
config = configparser.ConfigParser()
@ -24,3 +28,68 @@ def get_config(key):
:return:
"""
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 os
from difflib import SequenceMatcher
from utils import utils, constants as c
from utils import utils
import lark
from utils.constants import *
class ListingTransformer(lark.Transformer):
"""
Transforms content parsed by grammar_listing.lark further.
Used for the developer and inspirations list.
"""
def number(self, x):
raise lark.Discard
def unquoted_value(self, x):
return x[0].value
def quoted_value(self, x):
return x[0].value[1:-1] # remove quotation marks
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):
"""
The name part is treated as a property with key "name"
:param x:
:return:
"""
return 'name', x[0].value
def entry(self, x):
"""
All (key, value) tuples are inserted into a dictionary.
:param x:
:return:
"""
d = {}
for key, value in x:
if key in d:
raise RuntimeError('Key in entry appears twice')
d[key] = value
return d
def header(self, x):
raise lark.Discard
def start(self, x):
return x
@ -61,53 +83,9 @@ class EntryTransformer(lark.Transformer):
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_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):
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)
entries = os.listdir(c.entries_path)
entries = os.listdir(entries_path)
# iterate over all 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)
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)
if canonical_file_name != entry and canonical_file_name != entry[:-5] + '.md':
print('Warning: file {} should be {}'.format(entry, canonical_file_name))
source_file = os.path.join(c.entries_path, entry)
target_file = os.path.join(c.entries_path, canonical_file_name)
source_file = os.path.join(entries_path, entry)
target_file = os.path.join(entries_path, canonical_file_name)
if not os.path.isfile(target_file):
pass
# os.rename(source_file, target_file)
@ -390,9 +368,10 @@ def extract_links():
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 grammar_file:
:param transformer:
@ -410,8 +389,7 @@ def read_developer_info():
:return:
"""
developer_file = os.path.join(c.root_path, 'developer.md')
grammar_file = os.path.join(c.code_path, 'grammar_listing.lark')
grammar_file = os.path.join(code_path, 'grammar_listing.lark')
transformer = ListingTransformer()
developers = read_and_parse(developer_file, grammar_file, transformer)
# now transform a bit more
@ -446,7 +424,7 @@ def write_developer_info(developers):
:return:
"""
# comment
content = '{}\n'.format(comment_string)
content = '{}\n'.format(generic_comment_string)
# number of developer
content += '# Developer ({})\n\n'.format(len(developers))
@ -474,22 +452,26 @@ def write_developer_info(developers):
content += '\n'
# write
developer_file = os.path.join(c.root_path, 'developer.md')
utils.write_text(developer_file, content)
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:
"""
inspirations_file = os.path.join(c.root_path, 'inspirations.md')
grammar_file = os.path.join(c.code_path, 'grammar_listing.lark')
# read inspirations
grammar_file = os.path.join(code_path, 'grammar_listing.lark')
transformer = ListingTransformer()
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
for index, inspiration in enumerate(inspirations):
# check for valid keys
# check that keys are valid keys
for field in inspiration.keys():
if field not in valid_inspiration_fields:
raise RuntimeError('Unknown field "{}" for inspiration: {}.'.format(field, inspiration['name']))
@ -497,26 +479,27 @@ def read_inspirations_info():
for field in ('inspired entries',):
if field in inspiration:
content = inspiration[field]
content = content.split(',')
content = [x.strip() for x in content]
inspiration[field] = content
# check for duplicate names entries
names = [inspiration['name'] for inspiration in inspirations]
duplicate_names = (name for name in names if names.count(name) > 1)
duplicate_names = set(duplicate_names) # to avoid duplicates in duplicate_names
if duplicate_names:
print('Warning: duplicate inspiration names: {}'.format(', '.join(duplicate_names)))
return inspirations
def write_inspirations_info(inspirations):
"""
Given an internal list of inspirations, write it into the inspirations file
:param inspirations:
:return:
"""
# comment
content = '{}\n'.format(comment_string)
content = '{}\n'.format(generic_comment_string)
# number of developer
content += '# Inspirations ({})\n\n'.format(len(inspirations))
@ -545,7 +528,6 @@ def write_inspirations_info(inspirations):
content += '\n'
# write
inspirations_file = os.path.join(c.root_path, 'inspirations2.md')
utils.write_text(inspirations_file, content)

View File

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