game/stars.py

189 lines
6.0 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright © 2024 Simon Forman
#
# This file is part of game
#
# game is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# game is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with game. If not see <http://www.gnu.org/licenses/>.
#
'''
A field of stars. Each has planets. Each planet has...?
Keep it simple.
Stars have what qualities?
- Cosmetic
- color
- brightness
- size
- Game stats
- age?
- ?
🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘
♈ ♉ ♊ ♋ ♌ ♍ ♎ ♏ ♐ ♑ ♒ ♓
'''
from math import log10
from random import randint, expovariate, seed, gauss, lognormvariate
from poisson import poisson
from wordlist import generate_name
ROMAN_NUMBERS = [
None, # no zero, but this aligns entries and their indicies
'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X',
'XI', 'XII', 'XIII', 'XIV', 'XV', 'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
'XXI', 'XXII', 'XXIII', 'XXIV', 'XXV', 'XXVI', 'XXVII', 'XXVIII', 'XXIX',
]
seed(23)
MINIMUM_DISTANCE_BETWEEN_STARS = 160
# The old standard 4:3 ratio screen of 1024 x 768 pixels,
# let's have 10 x 10 of those.
WIDTH, HEIGHT = 10240, 7680
# Database stuff. SQL, etc.
TABLES = [
'''\
create table stars (
id INTEGER PRIMARY KEY AUTOINCREMENT,
x INTEGER,
y INTEGER,
radius INTEGER,
name TEXT UNIQUE
)''',
('''\
create table planets (
id INTEGER PRIMARY KEY,
ordo INTEGER,''' # The order from the star, counting from 1.
# I use "ordo" instead of "order" because "order" is a keyword
# in SQL (or at least it caused a syntax error in SQLite.)
# These capacities are fixed and reflect the conditions on the
# planet in a very abstract way. Bio and Industry should be
# low/high near and far from the star and high/low in the mid-
# range, but for now I'm just going to use random numbers.
'''bio_capacity INTEGER,
industrial_capacity INTEGER,
'''
'''star INTEGER,
FOREIGN KEY(star) REFERENCES stars(id)
)'''),
]
def init_db(conn):
print('Generating star data.')
c = conn.cursor()
for values in generate_stars(WIDTH, HEIGHT, MINIMUM_DISTANCE_BETWEEN_STARS):
try:
c.execute('insert into stars(x, y, radius, name) values (?, ?, ?, ?)', values)
except sqlite3.IntegrityError:
print(values) # "Rubbpo"
continue
generate_planets_for_star(c)
c.close()
conn.commit()
def iter_stars(conn):
c = conn.cursor()
c.execute('select x, y, radius, name from stars')
yield from c.fetchall()
c.close()
def get_planets_for_star_named(conn, name):
c = conn.cursor()
c.execute('select id from stars where "name" = ?', (name,))
(star_id,) = c.fetchone()
c.execute(
'select ordo, bio_capacity, industrial_capacity from planets'
' where "star" = ?'
' order by ordo'
' limit 29', # Because we only have 29 Roman numbers.
(star_id,)
)
# Note that fetchone() returns a tuple value suitable for passing
# to execute() as the values tuple. I destructure the result tuple
# and build a new one soley because it would be slightly obscure to
# do otherwise. I might use a variable named, say, star_id_tuple or
# star_id_row and pass it directly to execute(), or even go full
# cowboy and put the call to fetchone() in the parameter position in
# the call to execute() and eschew a variable altogether. But if
# somehow the query is made for a star name that isn't in the DB then
# the traceback would be unnecessarily unclear.
#
# In the event, having gone to this length to explain the situation
# I'll probably come back and switch to reusing the result tuple.
# I hate the waste. I bet if you looked at the bytecode the work is
# there: unpacking and repacking the id int. Bleah. Then again,
# this is running in response to a user event and the extra work is
# done once and it's not a lot compared to all the other work that's
# about to happen (drawing these planet data into the GUI) so it
# would be daft to worry about it (after working this all out, that
# is.)
for ordo, bio_capacity, industrial_capacity in c.fetchall():
assert bio_capacity >= 0
assert industrial_capacity >= 0
b = round(log10(bio_capacity), 1) if bio_capacity else 0
i = round(log10(industrial_capacity), 1) if industrial_capacity else 0
yield f'{name}-{ROMAN_NUMBERS[ordo]}', b, i
c.close()
# Procedural Generation of solar system data
def generate_stars(width, height, minimum_distance_between_stars):
unique_names = set()
for (x, y) in poisson(width, height, minimum_distance_between_stars):
name = generate_name()
while name in unique_names:
name = generate_name()
unique_names.add(name)
yield x, y, round(1 + expovariate(1)), name
def how_many_planets():
n = round(gauss(5, 5))
while n < 0:
n = round(gauss(5, 5))
return n
import sqlite3
def generate_planets_for_star(c):
star_id = c.lastrowid
for ordo in range(1, how_many_planets() + 1):
c.execute('insert into planets'
'(ordo, star, bio_capacity, industrial_capacity)'
' values (?, ?, ?, ?)',
(
ordo,
star_id,
round(lognormvariate(10, 5)),
round(lognormvariate(10, 5)),
)
)