~johanvandegriff/games.johanv.net

9ff49fb9104db14978997909cc1fdf38038b4ace — Johan Vandegriff 1 year, 3 months ago afd3eee
added circular maze generator
A circular_maze.py => circular_maze.py +250 -0
@@ 0,0 1,250 @@
#!/usr/bin/env python3
import math, random
from PIL import Image, ImageDraw

def generate_maze(
    num_levels = 16, #number of concentric circles in the maze
    big_level_every = 4, #big levels will have fewer gaps and be drawn with a thicker line
    img_size = 2000, #number of pixels for the image's width and height
    max_arc_length = .8, #max arc length of a segment before it needs to split into 2
    max_gaps_ratio = .25, #the max proportion of gaps compared to total segments
    narrow_line_width = .002, #how thick to make a normal level's lines
    wide_line_width = .007, #how thick to make a big level's lines
    foreground_color = 'black', #color of the maze's lines
    background_color = '#FFF0', #color of the image background
    debug = False, #should it show the missing gridlines in light blue
):
    img = Image.new('RGBA', (img_size, img_size), background_color)
    draw = ImageDraw.Draw(img)

    def draw_arc(draw, img_size, radius, start_degrees, end_degrees, line_width=narrow_line_width, color=foreground_color):
        dist_from_edge = round(img_size/2 - radius)
        start_degrees, end_degrees = 360-end_degrees, 360-start_degrees
        draw.arc([(dist_from_edge, dist_from_edge), (img_size-dist_from_edge, img_size-dist_from_edge)], start=start_degrees, end=end_degrees, fill=color, width=round(line_width*img_size))

    def polar_to_xy(img_size, radius, angle_degrees):
        return img_size/2+radius*math.cos(-angle_degrees*math.pi/180), img_size/2+radius*math.sin(-angle_degrees*math.pi/180)

    def draw_ray(draw, img_size, radius1, radius2, angle_degrees, line_width=narrow_line_width, color=foreground_color):
        push_inward = .001*img_size #tweak the radii slightly inward to make them be centered in the arc
        prev_x, prev_y = polar_to_xy(img_size, radius1-push_inward, angle_degrees)
        x, y = polar_to_xy(img_size, radius2-push_inward, angle_degrees)
        draw.line((prev_x,prev_y, x,y), fill=color, width=round(line_width*img_size))

    def get_level_radius(img_size, num_levels, level_idx):
        return round(img_size/2/num_levels*(.5+level_idx))



    MAZE_RAYS = 0
    MAZE_ARCS = 1
    maze = []

    prev_radius = 0
    num_segments = 4
    for level_idx in range(num_levels):
        radius = get_level_radius(img_size, num_levels, level_idx)

        #old method of automatically determining number of segments, works except they don't line up with the previous
        # segment_degrees = img_size*2/radius
        # if level_idx == 0:
        #     segment_degrees = img_size/radius
        # num_segments = round(360/segment_degrees)

        segment_length = 2*math.pi*radius/num_segments
        # if level_idx % 4 == 0:
        if segment_length > img_size*max_arc_length/num_levels:
            num_segments *= 2

        maze.append([[],[]])

        for cell_idx in range(num_segments):
            maze[level_idx][MAZE_RAYS].append(0)
            maze[level_idx][MAZE_ARCS].append(1)


    # maze = [
    #     # r,t,l,b
    #     [[0,0,0,0], #rays
    #   #tr,tl,bl,br
    #     [0,1,1,1]], #arcs
    #     [[1,1,1,1,1,1,1,1], #rays
    #     [1,1,1,1,1,1,1,1]], #arcs
    #     [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
    #     [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
    #     [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
    #     [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
    #     [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
    #     [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
    #     [[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
    #     [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
    #     [[0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
    #     [0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
    #     [[0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], #rays
    #     [0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]], #arcs
    # ]

    def get_neighbors(maze, node):
        neighbors = []
        if node == 'END': #special case, mostly node is a tuple
            prev_level_idx = len(maze)-1
            for cell_idx in range(len(maze[prev_level_idx][MAZE_ARCS])):
                if not maze[prev_level_idx][MAZE_ARCS][cell_idx]:
                    neighbors.append((prev_level_idx, cell_idx))
            return neighbors
        level_idx, cell_idx = node

        level_num_cells = len(maze[level_idx][MAZE_RAYS])
        prev_level_idx = level_idx-1
        next_level_idx = level_idx+1
        prev_cell_idx = (cell_idx-1) % level_num_cells
        next_cell_idx = (cell_idx+1) % level_num_cells

        if not maze[level_idx][MAZE_RAYS][cell_idx]:
            neighbors.append((level_idx, prev_cell_idx))
        if not maze[level_idx][MAZE_RAYS][next_cell_idx]:
            neighbors.append((level_idx, next_cell_idx))
        if not maze[level_idx][MAZE_ARCS][cell_idx]:
            if next_level_idx == len(maze):
                neighbors.append('END')
            else:
                next_level_num_cells = len(maze[next_level_idx][MAZE_RAYS])
                if next_level_num_cells == level_num_cells:
                    neighbors.append((next_level_idx, cell_idx))
                elif next_level_num_cells == level_num_cells * 2:
                    neighbors.append((next_level_idx, cell_idx*2))
                    neighbors.append((next_level_idx, cell_idx*2+1))
                else:
                    raise Exception(f'expected {level_num_cells * 2} cells on level {next_level_idx} but got {next_level_num_cells}')
        if prev_level_idx >= 0:
            prev_level_num_cells = len(maze[prev_level_idx][MAZE_RAYS])
            if prev_level_num_cells == level_num_cells:
                if not maze[prev_level_idx][MAZE_ARCS][cell_idx]:
                    neighbors.append((prev_level_idx, cell_idx))
            elif prev_level_num_cells == int(level_num_cells / 2):
                if not maze[prev_level_idx][MAZE_ARCS][int(cell_idx/2)]:
                    neighbors.append((prev_level_idx, int(cell_idx/2)))
            else:
                raise Exception(f'expected {int(level_num_cells / 2)} cells on level {next_level_idx} but got {next_level_num_cells}')
        
        return neighbors


    for level_idx, level in enumerate(maze):
        rays = level[MAZE_RAYS]
        arcs = level[MAZE_ARCS]

        is_big_level = (level_idx+1)/big_level_every == int((level_idx+1)/big_level_every)
        if is_big_level:
            max_gaps = round(len(arcs)*max_gaps_ratio)
            num_gaps = random.randint(1, max(1, max_gaps))
        else:
            max_gaps = round(len(arcs)*max_gaps_ratio)
            num_gaps = random.randint(3, max(3, max_gaps))

        # num_gaps = num_gaps * 2 - 1 #always odd number

        for i in range(len(rays)):
            rays[i] = 0 #start with no sub-divisions on the level
            arcs[i] = 1 #start with no gaps on the level
        if level_idx == 0: #level 0 always has 4 arcs
            num_gaps = 1
            num_gaps = random.randint(-1,2)
            if num_gaps < 1:
                num_gaps = 1

        # if level_idx == len(maze)-1:
        #     num_gaps = 1

        # print(max_gaps, num_gaps)

        indexes = [i for i in range(len(arcs))]
        while num_gaps > 0:
            if len(indexes) == 0:
                break #abort if there are no places left to put gaps
                # raise Exception("not enough options for gaps left")
            gap_idx = random.choice(indexes)
            indexes.remove(gap_idx)

            if is_big_level:
                dist = max(2, round(len(arcs) / 5)) #make sure the gaps on big levels are further away
            else:
                dist = 1

            for i in range(-dist, dist+1):
                #make sure there are no adjacent gaps:
                try:
                    indexes.remove((gap_idx-i) % len(arcs))
                except:
                    pass
            arcs[gap_idx] = 0
            num_gaps -= 1


    def validate_maze(maze):
        connected_nodes = [(0,0)] #0,0 is the top-right corner of the center circle
        #the 1st coord is the level, the 2nd coord is the cell, starting from the cell sitting on the right flat line

        connected_node_idx = 0
        while True:
            curr_node = connected_nodes[connected_node_idx]
            for node in get_neighbors(maze, curr_node):
                if not node in connected_nodes:
                    connected_nodes.append(node)
            connected_node_idx += 1
            if connected_node_idx >= len(connected_nodes):
                break

        #make sure all nodes are reachable by comparing the total number of reachable nodes with the expected number
        return len(connected_nodes) == sum(map(lambda l: len(l[MAZE_RAYS]), maze))+1 #length of maze plus 1 for the 'END' node

    assert validate_maze(maze)

    rays_possible_to_add = []
    for level_idx in range(1, len(maze)):
        for ray_idx in range(0, len(maze[level_idx][MAZE_RAYS])):
            rays_possible_to_add.append((level_idx, ray_idx))

    #try each ray once but in a random order
    while len(rays_possible_to_add) > 0:
        ray = random.choice(rays_possible_to_add)
        rays_possible_to_add.remove(ray)
        level_idx, ray_idx = ray
        if not maze[level_idx][MAZE_RAYS][ray_idx]:
            maze[level_idx][MAZE_RAYS][ray_idx] = 1
            if not validate_maze(maze):
                maze[level_idx][MAZE_RAYS][ray_idx] = 0

    prev_radius = 0
    num_segments = 4
    for level_idx in range(num_levels):
        radius = get_level_radius(img_size, num_levels, level_idx)

        is_big_level = (level_idx+1)/big_level_every == int((level_idx+1)/big_level_every)
        if is_big_level:
            arc_line_width = wide_line_width
        else:
            arc_line_width = narrow_line_width

        num_segments = len(maze[level_idx][MAZE_ARCS])
        segment_degrees = 360/num_segments

        for segment_idx in range(num_segments):
            # print(level_idx, segment_idx, end=' ')
            # print(maze[level_idx][MAZE_RAYS][segment_idx], maze[level_idx][MAZE_ARCS][segment_idx])
            # if (segment_idx+level_idx+3)/5 != int((segment_idx+level_idx+3)/5):
            if maze[level_idx][MAZE_RAYS][segment_idx]:
                draw_ray(draw, img_size, prev_radius, radius, segment_degrees * segment_idx)
            elif debug:
                draw_ray(draw, img_size, prev_radius, radius, segment_degrees * segment_idx, color='lightblue') #tmp
            # if (segment_idx+level_idx)/8 != int((segment_idx+level_idx)/8):
            if maze[level_idx][MAZE_ARCS][segment_idx]:
                draw_arc(draw, img_size, radius, segment_degrees*segment_idx, segment_degrees*(segment_idx+1), arc_line_width)
            elif debug:
                draw_arc(draw, img_size, radius, segment_degrees*segment_idx, segment_degrees*(segment_idx+1), arc_line_width, color='lightblue') #tmp
        
        prev_radius = radius

    # img.show()
    # img.save('test.png')
    return img

M games.py => games.py +37 -10
@@ 1,14 1,15 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask, request, render_template, url_for
from flask import Flask, request, render_template, url_for, send_file

import os, re, sys
import os, re, sys, io

import CARL, boggle, boggle_old
import hornswiggle
from nav import nav
import profanity_test
import circular_maze

app = Flask(__name__)



@@ 81,8 82,8 @@ def stoichiometry():
    return render_template("stoichiometry.html", nav=nav, active="chem", result=result, equation=equation, compound=compound, grams_or_moles_value=grams_or_moles_value, grams_or_moles=grams_or_moles)
#END STOICHIOMETRY

#START MATH
@app.route("/math")
#START MATH.pl
@app.route("/math.pl")
def math_page():
    return render_template("math.html", nav=nav, active="math")



@@ 93,23 94,49 @@ def math_game():
@app.route("/math_score.pl", methods=["GET", "POST"])
def math_score():
    return run_perl_page(request, "math_score.pl", "Math")
#END MATH
#END MATH.pl

#START WHACK
#START WHACK.pl
@app.route("/whack.pl", methods=["GET", "POST"])
def whack_page():
    return run_perl_page(request, "whack.pl", "whack")
#END WHACK
#END WHACK.pl

#START MAZE
@app.route("/maze")
#START MAZE.pl
@app.route("/maze.pl")
def maze_page():
    return render_template("maze.html", nav=nav, active="maze")

@app.route("/showmaze.pl", methods=["GET", "POST"])
def showmaze_page():
    return run_perl_page(request, "showmaze.pl", "maze")
#END MAZE
#END MAZE.pl

#START CIRCULAR MAZE
@app.route("/maze")
def circular_maze_page():
    return render_template("circular-maze.html", nav=nav, active="maze")

@app.route("/maze-gen", methods=["GET", "POST"])
def maze_gen():
    def serve_pil_image(pil_img):
        img_io = io.BytesIO()
        pil_img.save(img_io, 'PNG', quality=70)
        img_io.seek(0)
        return send_file(img_io, mimetype='image/png')
    return serve_pil_image(circular_maze.generate_maze(
        num_levels=int(request.args.get('num_levels', 16)),
        big_level_every=int(request.args.get('big_level_every', 4)),
        img_size=int(request.args.get('img_size', 2000)),
        max_arc_length=float(request.args.get('max_arc_length', .8)),
        max_gaps_ratio=float(request.args.get('max_gaps_ratio', .25)),
        narrow_line_width=float(request.args.get('narrow_line_width', .002)),
        wide_line_width=float(request.args.get('wide_line_width', .007)),
        foreground_color=request.args.get('foreground_color', 'black'),
        background_color=request.args.get('background_color', '#FFF0'),
        debug=(request.args.get('debug', 'false') == 'true'),
    ))
#END CIRCULAR MAZE

#START BOGGLE
@app.route("/boggle", methods=["GET", "POST"])

M nav.py => nav.py +6 -5
@@ 1,11 1,12 @@
nav = [
    ["home", "https://johanv.net"],
    ["games", "/"],
    ["CARL", "/carl"],
    ["boggle", "/boggle"],
    ["maze", "/maze"],
    ["whack", "/whack.pl"],
    ["math", "/math"],
    ["CARL", "/carl"],
    ["chem", "/chem"],
    ["Dr. H", "/hornswiggle"]
    ["Dr. H", "/hornswiggle"],
    ["maze", "/maze"],
    ["maze.pl", "/maze.pl"],
    ["math.pl", "/math.pl"],
    ["whack.pl", "/whack.pl"],
]

M requirements-local.txt => requirements-local.txt +1 -0
@@ 11,6 11,7 @@ git+https://github.com/rominf/hunspell_serializable.git
git+https://github.com/kmike/pymorphy2.git
spacy==2.3.5
pymongo==3.11.2
Pillow==9.4.0

opencv-python==4.5.5.62
scipy==1.6.0

M requirements.txt => requirements.txt +1 -0
@@ 11,6 11,7 @@ git+https://github.com/rominf/hunspell_serializable.git
git+https://github.com/kmike/pymorphy2.git
spacy==2.3.5
pymongo==3.11.2
Pillow==9.4.0

opencv-python==4.5.1.48
scipy==1.6.0

M templates/carl.html => templates/carl.html +1 -1
@@ 23,6 23,6 @@
<h1>API</h1>
<p style="overflow: hidden;">This page is the nice user interface for CARL. There is also <a href="https://games.johanv.net/carl_api">another page</a> that is the raw response from CARL based on inputs. For example, if you wanted to input <code>"Hello World"</code> you would send a request to <code>https://games.johanv.net/carl_api?user=Hello+World</code>. And if you want to allow profanity, add <code>&profanity=true</code> to your request.</p>
<p>If you want to embed a "CARL says" widget into your website, copy and paste this HTML code into your page:</p>
<textarea style="width: 100%;" rows=5>&lt;a href="https://games.johanv.net/carl"&gt;CARL says:&lt;/a&gt;&lt;br/&gt;&lt;embed height="60" width="315" src="https://games.johanv.net/carl_api"/&gt;</textarea>
<textarea style="width: 100%;" rows=5>&lt;a href="https://games.johanv.net/carl"&gt;CARL says:&lt;/a&gt;&lt;br/&gt;&lt;object height="60" width="315" data="https://games.johanv.net/carl_api"&gt;&lt;/object&gt;</textarea>

{% include 'footer.html' %}

A templates/circular-maze.html => templates/circular-maze.html +41 -0
@@ 0,0 1,41 @@
{% include 'header.html' %}

<h2>Johan's Circular Maze Generator</h2>

<button onclick="gen()">regenerate</button>
levels: <input id="num_levels" type="number" min="1", max="100" value="16" style="width: 50px">
big every: <input id="big_level_every" type="number" min="1", max="100" value="4" style="width: 50px">
resolution: <input id="img_size" type="number" min="100", max="5000" value="2000" style="width: 50px">
max arc length: <input id="max_arc_length" type="number" min="0.1", max="3" step="0.01" value="0.8" style="width: 50px">
max gaps ratio: <input id="max_gaps_ratio" type="number" min="0", max="1" step="0.01" value=".25" style="width: 50px">
narrow line: <input id="narrow_line_width" type="number" min="0.001", max="0.1" step="0.0001" value=".002" style="width: 50px">
wide line: <input id="wide_line_width" type="number" min="0.001", max="0.1" step="0.0001" value=".007" style="width: 50px">
foreground: <input id="foreground_color" type="text" value="black" style="width: 50px">
background: <input id="background_color" type="text" value="#FFF0" style="width: 75px">
<label>debug?<input id="debug" type="checkbox"></label>

<br>
<img id="maze" onload="resize_img()" src="/maze-gen" alt="image of circular maze" style="display: block; margin: auto">

<script>
    function gen() {
        document.getElementById('maze').src = '/maze-gen?'+
            'num_levels='+encodeURIComponent(document.getElementById('num_levels').value)+'&'+
            'big_level_every='+encodeURIComponent(document.getElementById('big_level_every').value)+'&'+
            'img_size='+encodeURIComponent(document.getElementById('img_size').value)+'&'+
            'max_arc_length='+encodeURIComponent(document.getElementById('max_arc_length').value)+'&'+
            'max_gaps_ratio='+encodeURIComponent(document.getElementById('max_gaps_ratio').value)+'&'+
            'narrow_line_width='+encodeURIComponent(document.getElementById('narrow_line_width').value)+'&'+
            'wide_line_width='+encodeURIComponent(document.getElementById('wide_line_width').value)+'&'+
            'foreground_color='+encodeURIComponent(document.getElementById('foreground_color').value)+'&'+
            'background_color='+encodeURIComponent(document.getElementById('background_color').value)+'&'+
            'debug='+encodeURIComponent(document.getElementById('debug').value)+'&'+
            't='+(Date.now())
    }
    function resize_img() {
        document.getElementById('maze').style.width = Math.min(window.innerWidth*0.9, Math.max(100, window.innerHeight*0.9-200)) + 'px';
    }
    // window.onresize = resize_img;
</script>

{% include 'footer.html' %}

M templates/index.html => templates/index.html +5 -4
@@ 2,13 2,14 @@

<h1>Games</h1>
<ul>
<li><a href="/carl">CARL</a> - a chatbot (bare API <a href="/carl_api">here</a>)</li>
<li><a href="/boggle">Boggle</a> - a multiplayer word puzzle</li>
<li><a href="/maze">Maze</a> - an old game I wrote in perl</li>
<li><a href="/whack.pl">Whack-A-Mole</a> - another perl game</li>
<li><a href="/math">Math</a> - a math worksheet (also perl)</li>
<li><a href="/carl">CARL</a> - a chatbot (bare API <a href="/carl_api">here</a>)</li>
<li><a href="/chem">Stoichiometry</a> - a chemical equation solver (written in Java)</li>
<li><a href="/hornswiggle">Dr. Hornswiggle's Trancendental Ideations Generator</a> - hmmm... You'd better just see for yourself.</li>
<li><a href="/maze">Circular Maze</a> - generate circular mazes of different sizes</li>
<li><a href="/maze.pl">Maze.pl</a> - an old game I wrote in perl</li>
<li><a href="/math.pl">Math.pl</a> - a math worksheet (also perl)</li>
<li><a href="/whack.pl">Whack.pl</a> - Whack-A-Mole, another perl game</li>
</ul>

<br/>