Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug
I'm trying to improve testing in my code base, and we basically have the following workflow:

  • Collect data from multiple data sources
  • Merge, transform, filter
  • Produce a composite data structure, essentially a list of entities
  • Use the composite structure to perform the rest of the business logic.

There's a pretty clear delineation between the steps 1-3 and 4, which basically just requires the output of steps 1-3, so I'm working to generate a way to build a test composite data structure. It is itself a dataclass, but it has a lot of heavily nested objects, some inherited from a java interface, and none with clear factories/etc.

If I can construct a test version of this, I can plug it directly into the rest of the business logic in Step 4, so that's my current plan, but I'm a little bit of a loss as to how to approach this. Is it worth looking into Pydantic/etc? If you had to do something like this, how would you go about approaching it.

Falcon2001 fucked around with this message at 23:36 on Sep 29, 2022

Adbot
ADBOT LOVES YOU

Zephirus
May 18, 2004

BRRRR......CHK

FredMSloniker posted:

Good advice should I take this to Linux, but I'm running Windows. As for the database connection... would this work?

SQLite will lock the db for writing while it handles writes within a transaction, which can cause other writes fail to commit during this time, if you try to write from two threads at the (relatively) exact time.

You can enable WAL mode to speed up writes (https://sqlite.org/wal.html) and you could add a handler to retry your transactions after a delay if they cause an exception on write.

You are probably going to want to use a context with the cursor or db connection objects to either destroy that at the end of your method or another method to ensure the cursor is closed promptly as that will cause the implicit transaction to commit. You can also manually call con.commit() to ensure this happens in your control.

Wallet posted:

The simple way to do multithreaded processing here would be to basically pass a list of jobs that will create those table entries (along with whatever data they need to do that) and then have your main thread deal with writing what they calculate when they return it. If your bottleneck is literally processor time threading it will help performance, but it's also going to introduce a lot of complexity. Unless you already have a working example of what you want to do that isn't performant enough when single threaded I wouldn't bother at this point.

Trying to have multiple threads acting on your database at the same time on their own connections is going to create weird poo poo you have to deal with, particularly if you are trying to calculate states that are dependent upon the calculation of other states as it sounds like you are.

Are there too many states for you to just hold the data you're operating on in RAM? If there aren't I'd just do whatever you need to do and dump it all at the end.

Also this. SQLite3 is mp safe, but wierdness can happen.

Foxfire_
Nov 8, 2010

FredMSloniker posted:

As for the database connection
Where does con come from? It looks like a global. If it was set up by any normal function code, it won't exist in the child processes because nothing runs in them except for imports

Example:
Python code:
import multiprocessing
import sqlite3

example_global_1 = None
example_global_2 = sqlite3.connect("example.db") 
# example_global_2 will work in this example, but is bad in general because it's making the `import foo` line run a whole bunch of
# code, touch the filesystem, and potentially have hard-to-handle errors

def initialize():
    global example_global_1
    example_global_1 = sqlite3.connect("example.db")

def main():
    initialize()

    print_values(False)

    with multiprocessing.Pool(1) as pool:
        pool.map(print_values, [True])

def print_values(in_child):
    if in_child:
        print("In child process")
    else:
        print("In parent process")

    print(f"example_global_1 = {example_global_1}")
    print(f"example_global_2 = {example_global_2}")


if __name__ == "__main__":
    main()
produces output like:
code:
In parent process
example_global_1 = <sqlite3.Connection object at 0x0000017EA9E37120>
example_global_2 = <sqlite3.Connection object at 0x0000017EA9CF8990>
In child process
example_global_1 = None
example_global_2 = <sqlite3.Connection object at 0x000002AC950AEE40>
(If you need it, it is possible to tell the Pool constructor to run additional code).


If I am reading your stuff correctly, your work function:

- Accepts a state as a parameter
- Returns a score + a set of new states

and then you:
1) Store a (that state, its score) tuple into the database
2) Call the work function on any of the new states that aren't already in the database

Is that right?

Assuming that and setting aside whether this top-down thing is a good approach to solve the problem:

multiprocessing.Pool is mostly set up to take a known list of things and run a function on each one, farming the computation across processes. A job can't schedule a new job itself, only the main process can do that. So the new states to explore need to get returned to the main process (either directly as returns from the work function, or via the database file).

Using return values would look something like this:
code:
# Create the processes upfront.  It will get reused.  
#
# It will default to the number of CPUs. 
# The chunk size is just a tuning parameter for speed.  It won't affect correctness
#   Bigger chunks => Less relative overtime serializing/deserializing things between
#                    processes.  Most chance that eventually some worker has nothing to do
#                    because there are no unclaimed chunks
#
with multiprocessing.Pool() as pool:

    Loop until there are no unscored states in the database:

        # Fetch states that need work.  You probably want to limit the max number of this
        # since it seems like what you're doing is going to make them combinatorially explode
        # into trillions of them
        states_to_score = Fetch all the unscored states from the database

        # Get a (score, new_states) for each of them, splitting the work up in parallel
        #
        # The result is equivalent to doing
        #   results = [work_function(x) for x in states_to_score]   
        results = pool.map(work_function, states_to_score)

        # Put the new results back into the database
        for state_that_was_scored, (score, new_states) in zip(states_to_score, results):
            Insert those results back into the database

        # If you want to print some progress, this is a convenient place to do it

def work_function(some_state):
    # Function that takes a state and produces a (score for that state, list of new states to explore)
    # tuple.  It doesn't touch database, or any other state scores
    return something_that_only_depends_on_some_state

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

Wallet posted:

The simple way to do multithreaded processing here would be to basically pass a list of jobs that will create those table entries (along with whatever data they need to do that) and then have your main thread deal with writing what they calculate when they return it. If your bottleneck is literally processor time threading it will help performance, but it's also going to introduce a lot of complexity. Unless you already have a working example of what you want to do that isn't performant enough when single threaded I wouldn't bother at this point.

Oh, my current program is definitely bottlenecking on processor time. Anyway, here's the current code, which is definitely running faster:
code:
import multiprocessing as mp
from peasant import get_state_complex, minimum_score, maximum_score, loads, dumps, exit
from fredlib import time_up, nice_print, a_b_percent, pluralize

def process_state_string(state_string):
    "Given a state string, pare the output of get_state_complex(state_string) to what this program cares about."
    state_complex = get_state_complex(state_string)
    output = [state_complex[0], {}]
    for result_key in state_complex[2]:
        new_state_string = loads(result_key)[0]
        if new_state_string:
            output[1][new_state_string] = True
    return output

import sqlite3 # initialize the state_scores table

def main():
    con = sqlite3.connect("Solver Database.sql")
    
    max_cpus = int(mp.cpu_count() * 1)
    con.execute("DROP TABLE IF EXISTS state_scores")
    con.execute("CREATE TABLE state_scores(state_string PRIMARY KEY, minimum_score, maximum_score)")
    
    current_pass = 0
    to_process = {'{}': True} # '{}' is the starting state string.
    while len(to_process) > 0:
        current_pass, current_pass_length, current_pass_position, new_positions, old_positions = current_pass + 1, len(to_process), 0, 0, 0
        new_to_process = {}
        to_drop = {}
        with mp.Pool(processes = max_cpus) as pool:
            for state_string in to_process:
                # is this already in the table?
                if con.execute("SELECT * FROM state_scores WHERE state_string = ?", (state_string,)).fetchall():
                    old_positions = old_positions + 1
                    to_drop[state_string] = True
                else:
                    new_positions = new_positions + 1
                    to_process[state_string] = pool.apply_async(process_state_string, (state_string,))
                if time_up():
                    nice_print("Pass %d. Checking table for state %s (%d new state%s, %d old state%s)..." % (current_pass, a_b_percent(current_pass_position, current_pass_length), new_positions, pluralize(new_positions), old_positions, pluralize(old_positions)))
                current_pass_position = current_pass_position + 1
            for i in to_drop:
                del to_process[i]
            current_pass_length, current_pass_position = len(to_process), 0
            while len(to_process) > 0:
                to_drop = {}
                for state_string, job in to_process.items():
                    if job.ready():
                        entry = job.get()
                        if type(entry[0]) == bool:
                            thing_to_insert = (state_string, minimum_score, maximum_score)
                        else:
                            thing_to_insert = (state_string, entry[0], entry[0])
                        con.execute("INSERT INTO state_scores VALUES(?, ?, ?)", thing_to_insert)
                        for new_state_string in entry[1]:
                            new_to_process[new_state_string] = True
                        to_drop[state_string] = True
                        current_pass_position = current_pass_position + 1
                    if time_up():
                        nice_print("Pass %d. Adding state %s to table..." % (current_pass, a_b_percent(current_pass_position, current_pass_length)))
                for i in to_drop:
                    del to_process[i]
            to_process = new_to_process
            pool.close()
            pool.join()
if __name__ == "__main__":
    main()
Anything in there look wrong and/or confusing?

QuarkJets
Sep 8, 2008

Install flake8 and run it on your code, it'll give you a ton of formatting tips

A lot of stuff is happening in main(). Try to write functions that do just 1 thing really well. It's okay if this drifts toward 2 or 3 things but it shouldn't be 10 things.

Try to avoid magic numbers, including magic indices. The gently caress is state_complex[0] or state_complex[2]? It's better if you assign variable names to the components of this object and then operate with those variables; that'll make your code closer to self-documenting and it'll be easier to work on.

Instead of `if type(entry[0]) == bool:`, use `if isinstance(entry[0], bool)`. Better still, try not to have code that's forking in different directions based on the type of a variable. Try to make entry[0] always be a specific type

QuarkJets fucked around with this message at 04:49 on Sep 30, 2022

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

QuarkJets posted:

Install flake8 and run it on your code, it'll give you a ton of formatting tips

I was today years old when I found out pip was a default part of Python. themoreyouknow.jpg And yeah. I'm gonna work some more on other parts of the code, then come back to this bit.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
All right, it's become clear that I need to sit down and properly learn Python and not just Google how to solve the problem of the minute. What's a good resource that's not going to make me feel like a kindergartener? The official documentation, or something else?

12 rats tied together
Sep 7, 2006

A lot of the stuff QuarkJets mentioned is less Python and more "OOP in general", I think, so you're not likely to find a python resource that does a very good job of it. The best thing I'm aware of is honestly just, posting about it online, and reading about it online (especially what makes "good code" and how its different from bad code).

I think a good first step would be trying to refactor your loop into discrete problems that need to be solved and then writing a function that solves exactly one of those problems, a good example here is probably this one:

Python code:
                # is this already in the table?
                if con.execute("SELECT * FROM state_scores WHERE state_string = ?", (state_string,)).fetchall():
                    old_positions = old_positions + 1
                    to_drop[state_string] = True
                else:
                    new_positions = new_positions + 1
                    to_process[state_string] = pool.apply_async(process_state_string, (state_string,))
We can tell from the comment that we're looking at whether or not something was already processed(?), but then the code that follows immediately is manipulating various state containers you've constructed which just kind of exist arbitrarily.

You could change this to "if already_in_table(state_string)", but I think you were right not to, because it doesn't increase the at-a-glance readability of the code just to have the sqlite query live somewhere else. That's a sign that this code is doing too much, and that the level of abstraction here is too low --> not enough information is being hidden/is encapsulated by a logical construct with a name and a signature.

The more this happens, as you keep layering on new code paths and conditions, the harder it becomes over time to unwind the code and give it a name. We're basically at a point where, in order to suggest a potential refactor, we have to understand the entire codebase and the full context of the problem you're solving. Being good at OOP design is one way to combat this problem.

I gave it a read-through and honestly I can say that I just can't understand it enough to offer any specific advice, except that --
Python code:
    while len(to_process) > 0:
# [...]
        to_drop = {}
# [...]
            for i in to_drop:
                del to_process[i]
# [...]
            while len(to_process) > 0:
                to_drop = {} # !?
It reads really weirdly to me to nest two while loops inside of each other that have the same condition (to_process > 0) and that both immediately define a container for "stuff to drop". Do you think this could just be one while loop? I feel like if you could unroll that part, the rest of it would be much clearer.

12 rats tied together
Sep 7, 2006

A piece of the standard library that I personally reach for all the time when I have "collections of stuff to do" is the collections.deque, which you might find to be more ergonomic for your problem. Specifically, that you can create them, put things in them, and then get things out of them later. If you want to just handle things one at a time, the workflow is to your_deque.append() your stuff -> you can your_deque.pop() to get the "most recently appended" or you can your_deque.popleft() to get the "first appended".

I suspect that you can clean up a lot of your "list with index counter that gets incremented" and "dictionary that you check the length of and del items from" with a deque, since it can be looped on, and it mutates itself as you use it:
Python code:
>>> import collections
>>> dq = collections.deque()
>>> for x in range(15):
...   dq.append(x) 
... 
>>> dq
deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
>>> while dq:
...   this_item = dq.pop() 
...   print(this_item)
... 
14
13
12
11
10 # and so on -- trimmed for readability
# an empty deque is also Falsey, which can help keep your conditional code branches clean and readable
>>> dq
deque([])
>>> bool(dq) 
False
Also you can use code=Python inside the code block to give yourself some syntax highlighting, which will help too. :)

12 rats tied together fucked around with this message at 20:11 on Sep 30, 2022

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

12 rats tied together posted:

I gave it a read-through and honestly I can say that I just can't understand it enough to offer any specific advice, except that --
Python code:
    while len(to_process) > 0:
# [...]
        to_drop = {}
# [...]
            for i in to_drop:
                del to_process[i]
# [...]
            while len(to_process) > 0:
                to_drop = {} # !?
It reads really weirdly to me to nest two while loops inside of each other that have the same condition (to_process > 0) and that both immediately define a container for "stuff to drop". Do you think this could just be one while loop? I feel like if you could unroll that part, the rest of it would be much clearer.

Well, I can tell you what that specific bit was doing, at least. The outer condition was 'have I processed everything?' I then went through to_process and checked which ones I'd processed already; if I had, I marked them for deletion in to_drop, and if I hadn't, I stored an AsyncResult in that key (previously, it held True.) Then I dropped the already-processed keys.

The inner loop was 'have all of the apply_async calls returned their data?' I then went through to_process again. If a call had returned its data, I processed that data, which included putting marking the new keys in new_to_process (values True) and adding the call's key to to_drop.

At the end of the inner loop, all the calls that I was done with were dropped from the dictionary, and I repeated until all calls had returned their data, then did to_process = new_to_process. Eventually, the outer loop would wind up dropping everything (all of the states the inner loop returned were processed already), the inner loop would be skipped, and the outer loop would exit.

None of that really matters right now, though, because I'm going to be scrapping much of what I've already done and starting over. Would anyone be interested in hopping over to the thread I've been doing this all for and kinda looking over my shoulder as I re-implement the code, saying 'wait, why are you/you shouldn't be doing that'? I don't want to flood this thread with 'okay, I did this, is this okay?'

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
On a related note, it looks like the Python style guide discourages having lines > 79 characters, and some of the strings in my code are going to be very long indeed. The problem I'm encountering is how to re-wrap a string in the code if I make some changes to it. Of the options I'm aware of, only triple-quoting a string doesn't add non-white-space characters to the beginning or end of a line, preventing me from automatically re-wrapping the line in Notepad++ or something. However, it does that by including the newlines and indentation in the string, which is a problem - especially since sometimes I want newlines in the string, so I can't just strip them out automatically! Should I just suck up flake8 making frowny faces at my long strings, or is there a more Pythonic way to handle a block of text without re-rolling it by hand every time I want to correct a typo?

Foxfire_
Nov 8, 2010

Python code:
A_LONG_STRING_CONSTANT = (
    "here is a line.  It is somewhat long and then there's a newline\n"
    + "then there's more lines.  These strings get concatenated into a single \n"
    + "string when this code runs"
)

ANOTHER_LONG_STRING_CONSTANT = (
    "here's a line but then there's no newline at the end of each of these strings "
    + "so the concatenated string is a single very long line"
)

YET_ANOTHER_STRING_CONSTANT = (
    "If you have two string literals adjacent to each other "
    "the parser will concatenate them together into a single "
    "string literal.  This is subtly different from the version with the "
    "+ because this is only ever a single string instead of making multiple strings and "
    "then gluing them."
)

necrotic
Aug 2, 2005
I owe my brother big time for this!

FredMSloniker posted:

On a related note, it looks like the Python style guide discourages having lines > 79 characters, and some of the strings in my code are going to be very long indeed. The problem I'm encountering is how to re-wrap a string in the code if I make some changes to it. Of the options I'm aware of, only triple-quoting a string doesn't add non-white-space characters to the beginning or end of a line, preventing me from automatically re-wrapping the line in Notepad++ or something. However, it does that by including the newlines and indentation in the string, which is a problem - especially since sometimes I want newlines in the string, so I can't just strip them out automatically! Should I just suck up flake8 making frowny faces at my long strings, or is there a more Pythonic way to handle a block of text without re-rolling it by hand every time I want to correct a typo?

I bump any linter line length checks to 120. The 79 thing is insane imo.

Data Graham
Dec 28, 2009

📈📊🍪😋



Luv too code on a 386 in VGA text mode

QuarkJets
Sep 8, 2008

Foxfire_ posted:

Python code:
A_LONG_STRING_CONSTANT = (
    "here is a line.  It is somewhat long and then there's a newline\n"
    + "then there's more lines.  These strings get concatenated into a single \n"
    + "string when this code runs"
)

ANOTHER_LONG_STRING_CONSTANT = (
    "here's a line but then there's no newline at the end of each of these strings "
    + "so the concatenated string is a single very long line"
)

YET_ANOTHER_STRING_CONSTANT = (
    "If you have two string literals adjacent to each other "
    "the parser will concatenate them together into a single "
    "string literal.  This is subtly different from the version with the "
    "+ because this is only ever a single string instead of making multiple strings and "
    "then gluing them."
)

The 3rd one is what I use exclusively, for whatever that's worth.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
Python code:
so_heres_my_question = (
    "So what do I do if I had a paragraph that was nice and"
    "oh wait I need to replace a big hunk of this paragraph with some long lump of text in here and reroll everything to look right"
    "my pudding?")
Am I going to have to go through and manually remove and re-add those quote marks, or is there a tool in Notepad++ (or some other text editor) to do it?

Foxfire_
Nov 8, 2010

black with --preview will reflow long strings, but it will also change a bunch of other stuff. It is actually hard to do string splitting automagically and be correct in all situations (f-strings, raw strings, ...) and the conversion black does will potentially change the meaning of the code in some edge cases (it should also yell if that happens, it compares the syntax tree before and after reformatting).

I typically just do it by hand if I'm messing with a long string (and not worry very much about occasional short lines when reflowing it), but big string constants don't come up that often for me.

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

necrotic posted:

I bump any linter line length checks to 120. The 79 thing is insane imo.

My team does 100, but yeah 79 is...antiquated. Just keep it reasonable so you're not cosplaying as a java dev and you'll be fine.

Data Graham
Dec 28, 2009

📈📊🍪😋



Google enforces 79 come hell or high water

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
Well, as I'm not a trillion-dollar corporation, I'm gonna indulge myself and go to 120 characters, because really. Probably the big boys would stuff all this text in a data file or something.

And since nobody seemed interest in hopping threads, which, fair, I'm gonna keep asking for help here until someone tells me to stop. Going right back to the beginning of my goal - finding the optimal path through the game book "Impudent Peasant!", and generalizing that solution to other game books - I need to first explain to the computer how to play. That starts with peasant.py, which I've pared way back to this for the moment:

Python code:
# My 120-character ruler. 7890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

import json
loads = json.loads


def dumps(t):
    """{'foo': 1, 'bar': 2} == {'bar': 2, 'foo': 1}, but
    json.dumps({'foo': 1, 'bar': 2}) != json.dumps({'bar': 2, 'foo': 1}).
    I want to be able to store both of those dicts as the same string, sooo..."""
    return json.dumps(t, sort_keys=True)


def copy(t):
    "Copy a dict in my particular way."
    return loads(dumps(t))


# A selection of functions used to apply BBCode formatting to test.
def b(text):
    return "[b]%s[/b]" % text


def i(text):
    return "[i]%s[/i]" % text


def s(text):
    return "[s]%s[/s]" % text


def u(text):
    return "[u]%s[/u]" % text


# The best and worst scores we can get in this game book.
minimum_score, maximum_score = 0, 1

# A state string is a serialized version of a game state, a dictionary containing all the information about the game
# that we need to know where we left off. Most obviously this is the section we're at, but it also includes our stats,
# our equipment, and any special flags.
#
# Given a state string, get_state_complex 'reads' the game book and returns the following information in a dictionary:
#
#     text: what does the section we're reading actually say? This doesn't include the description(s) of the choice(s).
#
#     is_random: is the choice we have to make here random?
#
#     choice_keys: this is a dictionary of choice keys. A choice key is a tuple whose first element is the text of the
#     given choice ("if you have a bow and wish to use it, turn to 57.") and whose second element is the state string
#     of the game state this choice leads to. The value of the key is the weight of the choice, i.e. how likely it
#     is to be selected randomly. If the choice isn't random, the weight is 1. If the choice can't actually be made
#     (e.g., we don't have a bow), the weight is 0, and the second element of the key is False (since there's no game
#     state this choice leads to).
#
#     score: if given, this section is a dead end. For most game books, we have either won or lost; for some, there may
#     be multiple endings with different associated scores. If this is given, is_random is False and choice_keys
#     contains no keys with positive weights (i.e, we've reached a section that says 'if you have the pudding, turn to
#     27; otherwise your adventure is over' and don't have the pudding).
#
# To avoid a lot of redundant code in get_state_complex, we need some helper functions. Here's the first; I'll define
# more as I need them.


def add_choice(choice_keys, choice_text, choice_state, weight=1):
    "The most basic of ways to add something to choice_keys."
    choice_key = (choice_text, dumps(choice_state))
    choice_keys[choice_key] = choice_keys.get(choice_key, 0) + weight


def get_state_complex(state_string):
    # flake8 complains the first time I use these as part of a long string unless I explicity declare them global.
    global b, i, s, u
    game_state = loads(state_string)
    o = {"is_random": False, "text": "", "choice_keys": {}}
    current_section = game_state.get("section")
    if not current_section:  # the starting game state is {}
        o["is_random"] = True
        o["text"] = (
            "You are about to take the lead role in an adventure that will make you a quite generally respected "
            "person, at least in your neck of the woods. Before you take part in this quest, you must first determine "
            "your own strengths and weaknesses. You use dice to work out your initial scores. On page 11 is an " +
            i("Adventure Sheet,") + " which you may use to record details of your adventure. On it, you will find "
            "boxes for recording your SKILL, STAMINA and LUCK scores, as well as other details. You are advised "
            "either to record your scores on the [i]Adventure Sheet[/i] in pencil or to make photocopies of the sheet "
            "for use in future adventures. Take note that you begin this adventure as a fifteen-year old Human and "
            "your SKILL and STAMINA score are generated a little differently to other Fighting Fantasy "
            "gamebooks.\n\n" + b("SKILL, STAMINA and LUCK") + "\n\nRoll one die."
        )
        game_state["section"] = "Roll STAMINA"
        for i in range(6, 9):
            game_state["Initial SKILL"], game_state["SKILL"] = i, i
            add_choice(o["choice_keys"], (
                b("You rolled a %d or a %d." % (i * 2 - 11, i * 2 - 10)) + " Your " +
                i("Initial") + " SKILL score is " + b("%d." % i)
            ), game_state)
    elif current_section == "Roll STAMINA":
        o["is_random"] = True
        o["text"] = (
            "Enter your " + i("Initial") + "SKILL score in the SKILL box on the " + i("Adventure Sheet.") +
            "\n\nRoll one die. Add 6 to the number rolled and enter this total in the STAMINA box."
        )
        game_state["section"] = "Roll LUCK"
        for i in range(6):
            game_state["Initial STAMINA"], game_state["STAMINA"] = i + 7, i + 7
            add_choice(o["choice_keys"], (
                b("You rolled a %d." % (i + 1)) + " Your " + i("Initial") + " STAMINA score is " +
                b("%d." % (i + 7))
            ), game_state)
    elif current_section == "Roll LUCK":
        o["is_random"] = True
        o["text"] = "Roll one die. Add 6 to the number and enter this total in the LUCK box."
        game_state["section"] = "Free Equipment"
        for i in range(6):
            game_state["Initial LUCK"], game_state["LUCK"] = i + 7, i + 7
            add_choice(o["choice_keys"], (
                b("You rolled a %d." % (i + 1)) + " Your " + i("Initial") + " LUCK score is " +
                b("%d." % (i + 7))
            ), game_state)

    # Additional 'elif current_section == "Section Name"' statements will go here.
    #
    # Like, a lot of them.

    else:
        # We've gone to a section that does not (yet) exist. This isn't a fatal error, but it's one the program calling
        # get_state_complex() needs to be aware of, and the simplest way to do that is...
        return None

    return o
So. What atrocities against Python have I committed so far? (And on an utterly unrelated note, why is SA eating any newlines I put between the code and this paragraph?)

QuarkJets
Sep 8, 2008

Python code:
import json
loads = json.loads
Do this instead:
Python code:
from json import loads
-----------------------------------------

Python code:
    return "[b]%s[/b]" % text
This is a pretty old of string formatting, use f-strings instead.

Python code:
    return f"[b]{text}[/b]"
I'm not a huge fan of single-character definitions for functions but I get it. Consider changing these

-----------------------------------------

Python code:
# A state string is a serialized version of a game state, a dictionary containing all the information about the game
<big block of other comments>
This is good documentation but really needs to be contained in the docstrings of your code. What is a docstring? It's a convention for documenting your code, whether that's a module, a function, a class, etc.

1. Turn this big block of documentation into a module docstring
2. Update your functions to use proper docstrings (triple-quotes)
3. Include function arguments in your function docstrings

-----------------------------------------

Python code:
def get_state_complex(state_string):
    # flake8 complains the first time I use these as part of a long string unless I explicity declare them global.
    global b, i, s, u
No, do not do this. What error is flake8 giving you? Either way, don't use globals unless you absolutely know what you're doing and you're certain that you need them. I'm certain that you don't need these to be global.

-----------------------------------------

Python code:
        o["text"] = (
            "You are about to take the lead role in an adventure that will make you a quite generally respected "
            "person, at least in your neck of the woods. Before you take part in this quest, you must first determine "
            "your own strengths and weaknesses. You use dice to work out your initial scores. On page 11 is an " +
            i("Adventure Sheet,") + " which you may use to record details of your adventure. On it, you will find "
            "boxes for recording your SKILL, STAMINA and LUCK scores, as well as other details. You are advised "
            "either to record your scores on the [i]Adventure Sheet[/i] in pencil or to make photocopies of the sheet "
            "for use in future adventures. Take note that you begin this adventure as a fifteen-year old Human and "
            "your SKILL and STAMINA score are generated a little differently to other Fighting Fantasy "
            "gamebooks.\n\n" + b("SKILL, STAMINA and LUCK") + "\n\nRoll one die."
        )
f-stringified:

Python code:
        o["text"] = (
            f"You are about to take the lead role in an adventure that will make you a quite generally respected "
            f"person, at least in your neck of the woods. Before you take part in this quest, you must first determine "
            f"your own strengths and weaknesses. You use dice to work out your initial scores. On page 11 is an "
            f"{i('Adventure Sheet,')} which you may use to record details of your adventure. On it, you will find "
            f"boxes for recording your SKILL, STAMINA and LUCK scores, as well as other details. You are advised "
            f"either to record your scores on the [i]Adventure Sheet[/i] in pencil or to make photocopies of the sheet "
            f"for use in future adventures. Take note that you begin this adventure as a fifteen-year old Human and "
            f"your SKILL and STAMINA score are generated a little differently to other Fighting Fantasy "
            f"gamebooks.\n\n{b('SKILL, STAMINA and LUCK')}\n\nRoll one die."
        )
-----------------------------------------

Python code:
    elif current_section == "Roll STAMINA":
        o["is_random"] = True
        o["text"] = (
            "Enter your " + i("Initial") + "SKILL score in the SKILL box on the " + i("Adventure Sheet.") +
            "\n\nRoll one die. Add 6 to the number rolled and enter this total in the STAMINA box."
        )
        game_state["section"] = "Roll LUCK"
        for i in range(6):
            game_state["Initial STAMINA"], game_state["STAMINA"] = i + 7, i + 7
            add_choice(o["choice_keys"], (
                b("You rolled a %d." % (i + 1)) + " Your " + i("Initial") + " STAMINA score is " +
                b("%d." % (i + 7))
            ), game_state)
So what's happening here? I thought the current_section was "Roll STAMINA", why does it appear to be setting the section label to "Roll LUCK"? It's difficult to follow what's happening.

I think that you should extract all of this code to its own function. You could call it "roll_stamina. If you did that for each of these "roll" types then you'd have a "get_state_complex" function that's kind of self-documenting and easy to follow. In fact, roll_stamina and roll_luck look like they'd be exactly the same function but with 1 word changed, so just define the function a little more generically and make that word an input argument (roll_skill("STAMINA") vs roll_skill("LUCK"))

QuarkJets
Sep 8, 2008

I also want to point out that you could use Enum to make your code a little more programmatic, it'll let you avoid having to do so many string comparisons. Here's an example:

Python code:
from enum import Enum

# ...

class Section(Enum):
    ROLL_LUCK = "LUCK"
    ROLL_STAMINA = "STAMINA"
    ROLL_ANAL_CIRCUMFERENCE = "BUTT"

# ...
 
if current_section == Section.ROLL_LUCK:
    o["text"] = f"The {currection_section.value} skill is being rolled"
elif current_section == Section.ROLL_STAMINA:
    # etc.

QuarkJets
Sep 8, 2008

Let's look at this for loop:

Python code:
        for i in range(6):
            game_state["Initial LUCK"], game_state["LUCK"] = i + 7, i + 7
            add_choice(o["choice_keys"], (
                b("You rolled a %d." % (i + 1)) + " Your " + i("Initial") + " LUCK score is " +
                b("%d." % (i + 7))
            ), game_state)
So I take it you want to add 7 to each possible result of a d6 roll, and you're going to store that.

First, I'll recommend again to use f-strings.

Second, I'd say you should refer to the luck variable rather than putting 'b("%d." % (i + 7))' at the end of your string. You stored i+7 earlier. What happens if you need to change this to i+8? Would you rather make that change 3 times or 1 time?

I'm also not a fan of multiple-assignment, except in the case where you're literally extracting values from some kind of container. What I mean is that I don't like lines that look like "a, b = 1, 2". Maybe it's just personal preference, but it feels like that kind of code is harder to read and parse than separate lines. If I'm trying to figure out where "b" was assigned, then it'd be great if I could just scan the left-side of the indented code instead of having to scan the entire line because there are multiple assignments per line.

For function calls I like to either have all of the arguments in a single line or all of the arguments spread out across multiple lines. No mixings, e.g. 2 arguments on one line then 1 on the next. The indentation of each argument should be the same for all arguments.

Instead of iterating over range(6) and incrementing all of your integers by 1, you could iterate over range(1, 7). But this is making use of what's commonly referred to as "magic numbers". You and I know that this is a d6 roll, so the valid values are 1 through 6. This might not always be obvious. It could be better to assign this range of values to some descriptive variables, like "min_luck" and "max_luck".

The 6 being added to the luck roll is also a magic number. Perhaps we can call it luck_modifier

I've noticed that you have a function named "i" and you're using loop variables named "i". This is why single-character function names are horrible; flake8 is probably complaining because you're overwriting a function definition in the global scope with. If you want to keep using single-character function names then you'll need to remember to not use i as an iteration variable. Normally I'd say rename the function and keep using "i" as your counter variable, but :shrug:

Putting it all together:

Python code:
# defined somewhere else, perhaps in an object that holds the allowed range for all of your skills
luck_roll_min = 1
luck_roll_max = 6
luck_modifier = 6

# ...
        for die_roll in range(luck_roll_min, luck_roll_max + 1):
            luck = die_roll + luck_modifier
            game_state["Initial LUCK"] = luck
            game_state["LUCK"] = luck
            add_choice(o["choice_keys"],
                       b(f"You rolled a {die_roll}. Your {i('Initial')} LUCK score is {luck}."),
                       game_state)
I normally setup flake8 to use a 120-character line limit, which I think would fit here. But if you wanted to split up the string then this is how I'd format it:

Python code:
        for die_roll in range(luck_roll_min, luck_roll_max + 1):
            luck = die_roll + luck_modifier
            game_state["Initial LUCK"] = luck
            game_state["LUCK"] = luck
            add_choice(o["choice_keys"],
                       b(f"You rolled a {die_roll}. "
                         f"Your {i('Initial')} LUCK score is {luck}."),
                       game_state)

QuarkJets fucked around with this message at 06:03 on Oct 1, 2022

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

FredMSloniker posted:

Python code:
# My 120-character ruler. 7890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

This is very minor, but what program are you using to write your code? If it's VSCode, you can add a vertical ruler by modifying your preferences: https://stackoverflow.com/questions/29968499/vertical-rulers-in-visual-studio-code which is very handy since it's just a nice visual cue.

QuarkJets
Sep 8, 2008

OP mentioned Notepad++. Strong recommend for switching over to VS Code here, too

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

QuarkJets posted:

Do this instead:
Python code:
from json import loads
The reason I have...
Python code:
import json
loads = json.loads
...is that immediately after it, I have...
Python code:
def dumps(t):
    """{'foo': 1, 'bar': 2} == {'bar': 2, 'foo': 1}, but
    json.dumps({'foo': 1, 'bar': 2}) != json.dumps({'bar': 2, 'foo': 1}).
    I want to be able to store both of those dicts as the same string, sooo..."""
    return json.dumps(t, sort_keys=True)
...which needs access to json.dumps. Is there a better way to do this?
Python code:
# I really don't want to do
from json import loads, dumps
# because now I have to remember to add sort_keys=True EVERY SINGLE TIME
I didn't know about f-strings. Now I do! It took me a little bit (and looking at your example) to realize how to nest f-strings, but I got the hang of it.

I went through and gave the markup functions full-word names. Actually, I created a markup function for the general case of 'bracket this with BBcode' and made functions to call that for convenience.

As for flake8's complaint... it's not doing it now. I'll remember that, next time, if that error appears, something else is wrong.

QuarkJets posted:

So what's happening here? I thought the current_section was "Roll STAMINA", why does it appear to be setting the section label to "Roll LUCK"? It's difficult to follow what's happening.
game_state is a deserialized version of the state string we took as input. I'm changing its section to "Roll LUCK", then (in the first trip through the i loop) setting its STAMINA and Initial STAMINA to 7. Then I call add_choice, which serializes the game state and stores that state string and the description of the choice leading to it, in this case "You rolled a 1. Your Initial STAMINA score is 7.", in o["choice_keys"]. Next time through the loop, the STAMINA and Initial STAMINA are 8, and the state string and choice description are modified accordingly.

When this function returns, it will have six different state strings, each with a different value for (Initial) STAMINA, but all of them will have the section as "Roll LUCK" - because when we proceed from that state string, that's the section we'll be on.

QuarkJets posted:

I think that you should extract all of this code to its own function.
Right now, my vision is that the code (eventually) will look something like this:
Python code:
def get_state_complex(state_string):
    game_state = loads(state_string)
    o = {"is_random": False, "text": "", "choice_keys": {}}
    current_section = game_state.get("section")
    if not current_section:  # the starting game state is {}
        # stuff we do when we start reading the book goes here, modifying o.
    elif current_section == "1":
        # stuff we do when reading section 1 goes here, modifying o.
    elif current_section == "2":
        # stuff we do when reading section 2 goes here, modifying o.
    elif current_section == "3":
        # stuff we do when reading section 3 goes here, modifying o.
        
    # ...
    
    elif current_section == "350":
        # stuff we do when reading section 350 goes here, modifying o.
    else:  # we've hit an undefined section. This isn't a fatal error.
        return None
    return o
Breaking every section out into its own function won't make get_state_complex look any nicer, and I'd have to define a function name for each possible section. I will be using functions for things sections have in common - for instance, 'offer three choices that only differ in what section we turn to next' or 'damage the character and turn to one section if they survive and another if they don't' - but, especially in "Impudent Peasant!", there's a lot of stuff that's unique to a single section that happens.

That said, I have tried to do this before in Lua. In Lua, I could do this:
Lua code:
local sections = {}
sections[1] = function(state_string)
    return stuff
end
sections[2] = function(state_string)
    return stuff
end
sections[3] = function(state_string)
    return stuff
end

-- ...

sections[350] = function(state_string)
    return stuff
end

local get_state_complex = function(state_string)
    local section = serpent.load(state_string).section -- I used the serpent library for serialization.
    -- The 'and' here makes sure I return nil if the state string's section doesn't exist.
    return sections[section] and sections[section](state_string)
end
And that does make get_state_complex look quite tidy. But Python doesn't let me say def sections["1"](state_string): It does let me store functions in dictionaries, but they have to be defined with a name first, which defeats the purpose - and lambda functions have to be a single line.

If you know a more Pythonic way to do what I'm doing, feel free to suggest it!

QuarkJets posted:

I also want to point out that you could use Enum to make your code a little more programmatic, it'll let you avoid having to do so many string comparisons. Here's an example:
That example is giving me a FATAL error. :v: That said, I'll come back to that thought if we don't wind up making it moot.

Falcon2001 posted:

This is very minor, but what program are you using to write your code?
Notepad++. Really, I don't need the ruler now, since I adjusted the window width, but I haven't had a reason to take it out.

You'll have to convince me to learn Visual Studio Code. I'm not saying I can't be convinced, just that the screenshots aren't selling me.

In any event, here's my modified code:
Python code:
# My 120-character ruler. 7890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

import json
loads = json.loads


def dumps(t):
    """{'foo': 1, 'bar': 2} == {'bar': 2, 'foo': 1}, but
    json.dumps({'foo': 1, 'bar': 2}) != json.dumps({'bar': 2, 'foo': 1}).
    I want to be able to store both of those dicts as the same string, sooo..."""
    return json.dumps(t, sort_keys=True)


def copy(t):
    """Copy a dict in my particular way."""
    return loads(dumps(t))


def markup(text, marker):
    """Apply BBcode markup to some text."""
    return f"[{marker}]{text}[/{marker}]"


def bold(text):
    return markup(text, "b")


def italic(text):
    return markup(text, "i")


def strike(text):
    return markup(text, "s")


def underline(text):
    return markup(text, "u")


minimum_score, maximum_score = 0, 1  # The best and worst scores we can get in this game book.

# To avoid a lot of redundant code in get_state_complex, we need some helper functions. Here's the first; I'll define
# more as I need them.


def add_choice(choice_keys, choice_text, choice_state, weight=1):
    """The most basic of ways to add something to choice_keys. See get_state_complex for more details."""
    choice_key = (choice_text, dumps(choice_state))
    choice_keys[choice_key] = choice_keys.get(choice_key, 0) + weight


def get_state_complex(state_string):
    """
    A state string is a serialized version of a game state, a dictionary containing all the information about the
    game that we need to know where we left off. Most obviously this is the section we're at, but it also includes our
    stats, our equipment, and any special flags.

    Given a state string, get_state_complex 'reads' the game book and returns the following information in a
    dictionary:

        text: what does the section we're reading actually say? This doesn't include the description(s) of the
        choice(s).

        is_random: is the choice we have to make here random?

        choice_keys: this is a dictionary of choice keys. A choice key is a tuple whose first element is the text of
        the given choice ("if you have a bow and wish to use it, turn to 57.") and whose second element is the state
        string of the game state this choice leads to. The value of the key is the weight of the choice, i.e. how
        likely it is to be selected randomly. If the choice isn't random, the weight is 1. If the choice can't actually
        be made (e.g., we don't have a bow), the weight is 0, and the second element of the key is False (since there's
        no game state this choice leads to).

        score: if given, this section is a dead end. For most game books, we have either won or lost; for some, there
        may be multiple endings with different associated scores. If this is given, is_random is False and choice_keys
        contains no keys with positive weights (i.e, we've reached a section that says 'if you have the pudding, turn
        to 27; otherwise your adventure is over' and don't have the pudding).
    """
    game_state = loads(state_string)
    o = {"is_random": False, "text": "", "choice_keys": {}}
    current_section = game_state.get("section")
    if not current_section:  # the starting game state is {}
        o["is_random"] = True
        o["text"] = (
            f"You are about to take the lead role in an adventure that will make you a quite generally respected "
            f"person, at least in your neck of the woods. Before you take part in this quest, you must first "
            f"determine your own strengths and weaknesses. You use dice to work out your initial scores. On page 11 "
            f"is an {italic('Adventure Sheet,')} which you may use to record details of your adventure. On it, you "
            f"will find boxes for recording your SKILL, STAMINA and LUCK scores, as well as other details. You are "
            f"advised either to record your scores on the {italic('Adventure Sheet')} in pencil or to make "
            f"photocopies of the sheet for use in future adventures. Take note that you begin this adventure as a "
            f"fifteen-year old Human and your SKILL and STAMINA score are generated a little differently to other "
            f"Fighting Fantasy gamebooks.\n\n{bold('SKILL, STAMINA and LUCK')}\n\nRoll one die."
        )
        game_state["section"] = "Roll STAMINA"
        for i in range(6, 9):
            game_state["Initial SKILL"], game_state["SKILL"] = i, i
            add_choice(o["choice_keys"], (
                f"{bold(f'You rolled a {i * 2 - 11} or a {i * 2 - 10}.')} "
                f"Your {italic('Initial')} SKILL score is {bold(f'{i}.')}"
            ), game_state)
    elif current_section == "Roll STAMINA":
        o["is_random"] = True
        o["text"] = (
            f"Enter your {italic('Initial')} SKILL score in the SKILL box on the {italic('Adventure Sheet.')}"
            f"\n\nRoll one die. Add 6 to the number rolled and enter this total in the STAMINA box."
        )
        game_state["section"] = "Roll LUCK"
        for i in range(6):
            game_state["Initial STAMINA"], game_state["STAMINA"] = i + 7, i + 7
            add_choice(o["choice_keys"], (
                f"{bold(f'You rolled a {i + 1}.')} Your {italic('Initial')} STAMINA score is {bold(f'{i + 7}.')}"
            ), game_state)
    elif current_section == "Roll LUCK":
        o["is_random"] = True
        o["text"] = "Roll one die. Add 6 to the number and enter this total in the LUCK box."
        game_state["section"] = "Free Equipment"
        for i in range(6):
            game_state["Initial LUCK"], game_state["LUCK"] = i + 7, i + 7
            add_choice(o["choice_keys"], (
                f"{bold(f'You rolled a {i + 1}.')} Your {italic('Initial')} LUCK score is {bold(f'{i + 7}.')}"
            ), game_state)

    # Additional 'elif current_section == "Section Name"' statements will go here.
    #
    # Like, a lot of them.

    else:
        # We've gone to a section that does not (yet) exist. This isn't a fatal error, but it's one the program calling
        # get_state_complex() needs to be aware of, and the simplest way to do that is...
        return None

    return o
Additional whiffle bats?

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
QuarkJets, one of your posts came in while I was composing that one, and I missed it. As it's getting late, I'll address it in the morning.

12 rats tied together
Sep 7, 2006

FredMSloniker posted:

Breaking every section out into its own function won't make get_state_complex look any nicer, and I'd have to define a function name for each possible section. I will be using functions for things sections have in common - for instance, 'offer three choices that only differ in what section we turn to next' or 'damage the character and turn to one section if they survive and another if they don't' - but, especially in "Impudent Peasant!", there's a lot of stuff that's unique to a single section that happens.

[...]
But Python doesn't let me say def sections["1"](state_string): It does let me store functions in dictionaries, but they have to be defined with a name first, which defeats the purpose - and lambda functions have to be a single line.

If you know a more Pythonic way to do what I'm doing, feel free to suggest it!

There's two main patterns I reach for when doing this, one of them you already touched on: put the functions in a dictionary. Just for completeness, that would look something like this:
Python code:
def handle_roll_luck(choices):
    print("Roll one die. Add 6 to the number and enter this total in the LUCK box.")
    for i in range(6):
        game_state["Initial LUCK"], game_state["LUCK"] = i + 7, i + 7
        add_choice(choices, f"You rolled a { i +1 }. Your initial LUCK score is { i +7 }")
    
    return "Roll STAMINA"

def handle_roll_stamina():
    print("Enter your initial SKILL score in the SKILL box on the Adventure Sheet.")
    print("Roll one die. Add 6 to the number rolled and enter this total in the STAMINA box.")

    return "Free Equipment"

game_state_handler_map = {
    "Roll STAMINA" = handle_roll_stamina,
    "Roll LUCK" = handle_roll_luck,
    "Free Equipment" = handle_free_equipment,
    # and so on -- up to "handle_page_350"
}

def walk_game_state():
    game_state = whatever

    while game_state['is_running']:
        next_state = game_state_handler_map[game_state['current_state']]()
        game_state['current_state'] = next_state
Right off the bat this is beneficial because your elif chain doesn't have to own "all possible behaviors". The functions do need to be defined, true, but that's normal (the behavior has to exist somewhere). Since they're functions, you could put them in a module and import them at the top of your "main" file, and then they're already in scope. If you don't want to deal with manually registering them in the dictionary, you can have a "game_state_functions" list in each of your modules, import the list, and loop on it to add each function to the dictionary with its string matcher.

Second pattern is basically the same except instead of manually pairing a string key to a function value, you can use structural pattern matching. This is useful because it gives you a clean, ergonomic way to interpret an input object (like your game state) and dispatch to an arbitrary code path while also performing setup for that code path. The example in the link is actually input handling for a text adventure game, you might find it somewhat relevant.

QuarkJets
Sep 8, 2008

FredMSloniker posted:

The reason I have...
Python code:
import json
loads = json.loads
...is that immediately after it, I have...
Python code:
def dumps(t):
    """{'foo': 1, 'bar': 2} == {'bar': 2, 'foo': 1}, but
    json.dumps({'foo': 1, 'bar': 2}) != json.dumps({'bar': 2, 'foo': 1}).
    I want to be able to store both of those dicts as the same string, sooo..."""
    return json.dumps(t, sort_keys=True)
...which needs access to json.dumps. Is there a better way to do this?

Sure, you could do this:

Python code:
import json
from json import loads
This gives you access to json.loads, json.dumps, and loads (which is the same as json.loads)

quote:

game_state is a deserialized version of the state string we took as input. I'm changing its section to "Roll LUCK", then (in the first trip through the i loop) setting its STAMINA and Initial STAMINA to 7. Then I call add_choice, which serializes the game state and stores that state string and the description of the choice leading to it, in this case "You rolled a 1. Your Initial STAMINA score is 7.", in o["choice_keys"]. Next time through the loop, the STAMINA and Initial STAMINA are 8, and the state string and choice description are modified accordingly.

When this function returns, it will have six different state strings, each with a different value for (Initial) STAMINA, but all of them will have the section as "Roll LUCK" - because when we proceed from that state string, that's the section we'll be on.

Are you setting the section to "Roll LUCK" because that's the section that comes after rolling stamina? That's unclear. I think that it'd be clearer if you were just incrementing an index or fetching the next value for an iterable instead of manually setting the next section label like this

Here's an idea: define an enum of your section labels.

Python code:
class Section(Enum)
    ROLL_STAMINA = 1
    ROLL_LUCK = 2
    # etc.

if section_label == Section.ROLL_STAMINA:
    # Do some stuff
    # ...
    # Finally, increment to the next section label
    section_label = Section(section_label.value + 1)
elif section_label == Section.ROLL_LUCK
    # etc.
This gets rid of a bunch of string comparisons and now you don't have any mention of "luck" in the stamina block.

quote:

Right now, my vision is that the code (eventually) will look something like this:
Python code:
def get_state_complex(state_string):
    game_state = loads(state_string)
    o = {"is_random": False, "text": "", "choice_keys": {}}
    current_section = game_state.get("section")
    if not current_section:  # the starting game state is {}
        # stuff we do when we start reading the book goes here, modifying o.
    elif current_section == "1":
        # stuff we do when reading section 1 goes here, modifying o.
    elif current_section == "2":
        # stuff we do when reading section 2 goes here, modifying o.
    elif current_section == "3":
        # stuff we do when reading section 3 goes here, modifying o.
        
    # ...
    
    elif current_section == "350":
        # stuff we do when reading section 350 goes here, modifying o.
    else:  # we've hit an undefined section. This isn't a fatal error.
        return None
    return o
Breaking every section out into its own function won't make get_state_complex look any nicer, and I'd have to define a function name for each possible section. I will be using functions for things sections have in common - for instance, 'offer three choices that only differ in what section we turn to next' or 'damage the character and turn to one section if they survive and another if they don't' - but, especially in "Impudent Peasant!", there's a lot of stuff that's unique to a single section that happens.

That said, I have tried to do this before in Lua. In Lua, I could do this:
Lua code:
local sections = {}
sections[1] = function(state_string)
    return stuff
end
sections[2] = function(state_string)
    return stuff
end
sections[3] = function(state_string)
    return stuff
end

-- ...

sections[350] = function(state_string)
    return stuff
end

local get_state_complex = function(state_string)
    local section = serpent.load(state_string).section -- I used the serpent library for serialization.
    -- The 'and' here makes sure I return nil if the state string's section doesn't exist.
    return sections[section] and sections[section](state_string)
end
And that does make get_state_complex look quite tidy. But Python doesn't let me say def sections["1"](state_string): It does let me store functions in dictionaries, but they have to be defined with a name first, which defeats the purpose - and lambda functions have to be a single line.

If you know a more Pythonic way to do what I'm doing, feel free to suggest it!

I think that def sections["1"](state_string): is equivalent to def roll_luck(state_string): for tidiness, but the latter is a lot more descriptive. Am I missing something?

quote:

That example is giving me a FATAL error. :v: That said, I'll come back to that thought if we don't wind up making it moot.

lol nice

QuarkJets
Sep 8, 2008

Python code:
def dumps(t):
    """{'foo': 1, 'bar': 2} == {'bar': 2, 'foo': 1}, but
    json.dumps({'foo': 1, 'bar': 2}) != json.dumps({'bar': 2, 'foo': 1}).
    I want to be able to store both of those dicts as the same string, sooo..."""
    return json.dumps(t, sort_keys=True)

def copy(t):
    """Copy a dict in my particular way."""
    return loads(dumps(t))
You're copying a dictionary by serializing it and then deserializing it?

This is way cleaner IMO and way faster too:
Python code:
def copy_dict(input_dict):
    return dict(sorted(input_dict.items()))
Hopefully you're not copying dicts very often... ?

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
I started writing a reply before midnight, and it's now after midnight, and I need to go to bed, so you'll see that reply in the morning. Before you do, though, I have a question:

https://docs.python.org/3/tutorial/classes.html posted:

A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future. Examples of namespaces are: the set of built-in names (containing functions such as abs(), and built-in exception names); the global names in a module

quote:

the global names in a module

Is there a way I can access the namespace of, say, usefulfunctions.py from another program? Does it take the form of a dictionary whose keys include, say, the names of the functions usefulfunctions.py defines? Would the values of those keys happen to be the functions in question? Because if even one of those is true, I think it would be very useful.

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
Are you creating a program to play the gamebook, or are you creating one to solve the best path through it? Because while the structure you're going for seems adequate for allowing a human to play through the book, it's going to make things very difficult as far as efficiently solving it goes.

To solve the book efficiently, you're going to want to manipulate and simplify the section graph. You can't do that if the links between sections are just opaque functions - you want your code to be able to inspect those connections and merge them together. (For example, combining a chain of free choices between two-to-three options into a single free choice between 20 different outcomes; or combining a series of free and random choices into a single weighted random choice).

Instead of expressing each section as an opaque function, I'd aim to express it as a structured data element. Something like this:

code:
section:
{
  name: string // section name. Will just be the section number (e.g. "169") for most sections.
  effects: [effect] // list of all effects that happen as soon as you enter. e.g. gain or lose an item, adjust stats, enter combat, etc.
  edges: [edge] // list of all possible outgoing edges.
}

edge:
{
  target: string // which section this edge leads to
  requirements: [requirement] // list of everything needed to take this edge - which items, skills, etc.
  effects: [effect] // list of all effects that happen when you take this edge. e.g. adjust stats, lose an item, etc.
}
By encoding the book in a more structured way like this, you can have a function that looks at the current section, identifies what the user can do here, and gives them the appropriate choice of where to go - but you can also have a different set of functions that works on the same underlying data and manipulates the section graph in order to simplify it and make it solvable.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
Right. Now that I've slept and am more or less fresh, let's do a reply. Note: I have some ideas later in the post that would lead to modifying code earlier in the post, so read through the whole thing before commenting, please!

QuarkJets posted:

Are you setting the section to "Roll LUCK" because that's the section that comes after rolling stamina? That's unclear.

I've renamed game_state to next_game_state, since it may start as the current game state but is really a scratch pad for creating new game states to turn into state strings. Hopefully this will deal with your concerns re: changing the section name. In that vein, to the previous code, I've added:

Python code:
def roll_stats(choice_keys, next_game_state, stat_name):
    """A function used to avoid duplication in sections 'Roll STAMINA' and 'Roll LUCK"."""
    for i in range(7, 13):  # Yes, I know, magic numbers, hush
        next_game_state[stat_name] = i
        next_game_state[f"Initial {stat_name}"] = i
        add_choice(
            choice_keys,
            (f"{bold(f'You rolled a {i - 6}.')} Your {italic('Initial')} {stat_name} score is {bold(f'{i}.')}"),
            next_game_state
        )

And changed the relevant sections of get_state_complex to:

Python code:
    elif current_section == "Roll STAMINA":
        o["is_random"] = True
        o["text"] = (
            f"Enter your {italic('Initial')} SKILL score in the SKILL box on the {italic('Adventure Sheet.')}"
            f"\n\nRoll one die. Add 6 to the number rolled and enter this total in the STAMINA box."
        )
        next_game_state["section"] = "Roll LUCK"
        roll_stats(o["choice_keys"], next_game_state, "STAMINA")
    elif current_section == "Roll LUCK":
        o["is_random"] = True
        o["text"] = "Roll one die. Add 6 to the number and enter this total in the LUCK box."
        next_game_state["section"] = "Free Equipment"
        roll_stats(o["choice_keys"], next_game_state, "LUCK")

I've also made this change:

Python code:
import json
from json import loads

QuarkJets posted:

I think that it'd be clearer if you were just incrementing an index or fetching the next value for an iterable instead of manually setting the next section label like this

99 times out of 100, I'm not going to be incrementing the section. On Section 9, for instance, I have the option to guard a warehouse (Section 47), enter a cave (Section 23, but only if I have at least one torch), or go fishing (Section 25, but only if I have fishing gear). That said, would the code look like this?

Python code:
# at some point before get_state_complex:
from enum import Enum
class section_enum(Enum):
    start_of_game = auto()
    roll_stamina = auto()
    roll_luck = auto()
    free_equipment = auto()
    # and so forth

# then, in get_state_complex:
    elif current_section == section_enum.roll_stamina:
        next_game_state["section"] = section_enum.roll_luck

Actually, I did a bit of research on the post I made last night. Could I do something like this (leaving section a string)?

Python code:
# a bunch of defs of functions for each section.
def get_state_complex(state_string):
    section = loads(state_string).get("section", "start_of_game")
    return vars().get(section, lambda a: None)(state_string)

12 rats tied together posted:

Second pattern is basically the same except instead of manually pairing a string key to a function value, you can use structural pattern matching. This is useful because it gives you a clean, ergonomic way to interpret an input object (like your game state) and dispatch to an arbitrary code path while also performing setup for that code path. The example in the link is actually input handling for a text adventure game, you might find it somewhat relevant.

Hm. I can see replacing the 'if - elif - elif - else' with 'match case - case - case - case other', but I'm not sure what I gain by doing so. Performance? I'm not passing get_state_complex a command; I'm passing it a representation of my current situation in the game and asking what commands are valid. I could just as well be sending it a tic-tac-toe board and whose turn it is to move and having it send back what the current player's valid moves are and what the board will look like for each. Can you elaborate?

QuarkJets posted:

You're copying a dictionary by serializing it and then deserializing it?

If I said I was unaware of a better way, that would imply I looked. :v: Does that create a deep copy? Or should I be importing deepcopy from copy (which I literally learned was a thing right now from Googling 'python deep copy dictionary')? (Please don't despair as your flickering flashlight tries futilely to illuminate the depths of my ignorance.)

Jabor posted:

Are you creating a program to play the gamebook, or are you creating one to solve the best path through it? Because while the structure you're going for seems adequate for allowing a human to play through the book, it's going to make things very difficult as far as efficiently solving it goes.

I'm doing both, ultimately. I want to have output that looks like this:

Section 34 posted:

The tangled wood lies on a small piece of land to the east of Bitterford, near the Red River. Farmer Corran accompanies you most of the way, but stops some distance from the wood and refuses to walk any closer. His two remaining dogs shiver and whimper alongside him, their tails tightly curled downwards in abject fear.

‘There,’ he says, pointing to a break in the undergrowth that looks like a path. ‘It vanished down there, it did.’

You enter the wood, following what appears to be some sort of game trail. The surrounding trees are not especially tall or large, but the foliage on either side of the trail is thick and choked with thorny bushes and spiny thickets of bamboo. After a short while, the only sounds being the buzz of insects and the occasional strange birdcall, the trail splits into three. From the trail to your left, you seem to detect an increase in the volume of insect noise in the form of a distant buzzing drone. Directly ahead of you, another trail winds deeper into the wood, and from here you think to pick out a weird honking sound, like that of largish bird. To you right there are no sounds, though you spot what looks like a human-like footprint in the muddy path that forms this trail. Which direction will you go now?
  • Turn left. Turn to 7. (Expected score: 51.38%.)
  • Go straight ahead. Turn to 28. (Expected score: 29.33%.)
  • Turn right. Turn to 15. (Expected score: 58.96%.)

So I'm limited in how much I can simplify the section graph. That said, if there's only one choice you can make in a given section, I can simplify things. If my previous plan for get_state_complex would work, I can use this instead:

Python code:
# a bunch of defs of functions for each section.
def get_state_complex(state_string):
    section = loads(state_string).get("section", "start_of_game")
    output = vars().get(section, lambda a: None)(state_string)
    # If we can't actually make a choice, choice_key[1] will be False.
    valid_choices = {key: value for key, value in output["choice_keys"].items() if key[1]}
    if len(valid_choices) == 1:  # if there's only one candidate...
        for choice_state_string in valid_choices.values():  # ...there's only one choice.
            new_output = get_state_complex(choice_state_string)
        # show_choices(choice_keys) is a function that takes the choice keys and returns a string that's a block of
        # BBCode-formatted text to paste into a forum post (see the end of my Section 34 quote for an example).
        new_output["text"] = f"{output["text"]}\n\n{show_choices(output["choice_keys"])}\n\n{new_output["text"]}"
        output = new_output
    return output

So it would elide sections until it reached an actual choice. This will be especially useful when I get into combat and have a lot of situations where I'm like 'okay, what weapon are you using this round?' and 95% of the time the choice is obvious.

As my professors used to say, questions? Comments? Difficulties?

QuarkJets
Sep 8, 2008

FredMSloniker posted:

99 times out of 100, I'm not going to be incrementing the section. On Section 9, for instance, I have the option to guard a warehouse (Section 47), enter a cave (Section 23, but only if I have at least one torch), or go fishing (Section 25, but only if I have fishing gear). That said, would the code look like this?

Python code:
# at some point before get_state_complex:
from enum import Enum
class section_enum(Enum):
    start_of_game = auto()
    roll_stamina = auto()
    roll_luck = auto()
    free_equipment = auto()
    # and so forth

# then, in get_state_complex:
    elif current_section == section_enum.roll_stamina:
        next_game_state["section"] = section_enum.roll_luck

Actually, I did a bit of research on the post I made last night. Could I do something like this (leaving section a string)?

Python code:
# a bunch of defs of functions for each section.
def get_state_complex(state_string):
    section = loads(state_string).get("section", "start_of_game")
    return vars().get(section, lambda a: None)(state_string)
Hm. I can see replacing the 'if - elif - elif - else' with 'match case - case - case - case other', but I'm not sure what I gain by doing so. Performance? I'm not passing get_state_complex a command; I'm passing it a representation of my current situation in the game and asking what commands are valid. I could just as well be sending it a tic-tac-toe board and whose turn it is to move and having it send back what the current player's valid moves are and what the board will look like for each. Can you elaborate?

At the end of the day, every section has its own logic for determining what happens. Some sections might have the same or similar logic; for instance, rolling starting LUCK is very similar to rolling starting STAMINA. If you designate a function for every section then you can simplify your code and improve its readability. Instead of setting the state of a variable and then using that state to determine what to do next, what if you just did the next thing by calling the next function in the chain?

code:
def tickle_elephant(args):
    # The final chapter of the book
    for roll in range(1, 6):
        winning_roll = roll > 3
        if winning_roll:
            # Do something
        else:
            # Do something else
 
def fight_witch(args):
    # After fighting the witch, tickle the elephant
    # Or maybe we returned None earlier because the witch killed you and the story ended early
    for witch_roll in range(3):
        if witch_roll > lethal_damage:
            # Do something; the game is over
        else:
            # Do something else and go to the next section.
            tickle_elephant(args)

def roll_luck(args):
    # After rolling for luck, fight the witch
    fight_witch(args)

def roll_skills(args, skill_type):
    if skill_type == 'stamina':
        # After rolling for stamina, roll for luck
        roll_skill(args, 'luck)
    elif skill_type == 'luck':
        # After rolling for luck, fight the witch
        fight_witch(args)

def begin_game(args):
    # Print a bunch of lines and then start rolling for skills
    roll_skills(args, "stamina")

begin_game(initial_book_state)
Each section is described entirely by a function. Any if/else logic is embedded directly in the function. The function determines what the next section is going to be: after rolling for luck, fight the witch, then tickle the elephant, etc. Or sometimes a different section could be called, depending on certain conditions (e.g. the game could end prematurely when the witch is fought, so then the player never encounters the elephant).

quote:

If I said I was unaware of a better way, that would imply I looked. :v: Does that create a deep copy? Or should I be importing deepcopy from copy (which I literally learned was a thing right now from Googling 'python deep copy dictionary')? (Please don't despair as your flickering flashlight tries futilely to illuminate the depths of my ignorance.)

Deepcopy will create a complete and exact copy of what you provide it. It's likely that you don't need to make deep copies of anything but I'm not sure.

But it sounds like you want sorted keys. Creating a new dict from the sorted dict will give you a sorted deep copy. dict(sorted(x.items())) is the pattern you'd want for a sorted copy; if you just wanted a copy with no sorting then deepcopy() would be fine.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.

QuarkJets posted:

At the end of the day, every section has its own logic for determining what happens. Some sections might have the same or similar logic; for instance, rolling starting LUCK is very similar to rolling starting STAMINA. If you designate a function for every section then you can simplify your code and improve its readability. Instead of setting the state of a variable and then using that state to determine what to do next, what if you just did the next thing by calling the next function in the chain?

That would work just fine if my goal were simply to play the game book, but my aim is to solve it. I don't want to get into my ideas on how I plan to implement this yet, but my goal is to make a program that takes peasant.py and generates a corresponding scoring table (I plan on using sqlite3) that gives the expected score for every possible state string. (Which is to say, every state string that can come of following choices from the beginning of the game book; you're not going to have a state string where your STAMINA is, say, 24 because that can't happen in this game book.) To get those state strings, it's going to have to be able to walk through the game book section by section; it can then score those state strings by working backwards from the winning and losing sections.

Once I have that table, I can then ask 'okay, if I'm on this section, and I have these stats and these things, what are my choices, and what's my expected score for each one?' And I can have a third program take peasant.py and that scoring table (or possibly embed all of peasant.py's output in the scoring table, though that would make it much larger) and kick out neatly-formatted BBcode for the given situation. So after I've finished showing off all of the content of the book in my LP, I can then go through and show the optimal path and the expected score at each step, so they can see just how much a good roll makes things better and a bad roll makes things worse.

Again, that's the end goal. (And I did say as much earlier, but maybe I wasn't clear enough?) For now, I just want to implement getting from one state string to another without making Baby Guido cry.

QuarkJets posted:

Deepcopy will create a complete and exact copy of what you provide it. It's likely that you don't need to make deep copies of anything but I'm not sure.

I'm not 100% sure whether I'll need to make deep copies yet. Heck, my code so far doesn't need to make shallow copies. But it's good to know whether a copy is deep or not so I know what I can safely do with it.

QuarkJets posted:

But it sounds like you want sorted keys. Creating a new dict from the sorted dict will give you a sorted deep copy. dict(sorted(x.items())) is the pattern you'd want for a sorted copy; if you just wanted a copy with no sorting then deepcopy() would be fine.

The keys don't need to be sorted in the game state. They need to be sorted in the state string because, as my current version of dumps says:

Python code:
def dumps(t):
    """{'foo': 1, 'bar': 2} == {'bar': 2, 'foo': 1}, but
    json.dumps({'foo': 1, 'bar': 2}) != json.dumps({'bar': 2, 'foo': 1}).
    I want to be able to store both of those dicts as the same string, sooo..."""
    return json.dumps(t, sort_keys=True)

The functions get_state_complex calls may modify the game state in arbitrary ways, but if I can get to section 9 in the same condition three different ways, it doesn't matter in exactly what order I got the equipment on each path or when those two special flags got set. I want the same state_string for the same situation. That means dict(x.items()) would work for copying my dictionaries. Though I assume they'd be shallow copies.

So would the thing with return vars().get(section, lambda a: None)(state_string) work as a way to call a function by the name in the section string and return its output, or return None if there's no function by that name?

QuarkJets
Sep 8, 2008

FredMSloniker posted:

That would work just fine if my goal were simply to play the game book, but my aim is to solve it. I don't want to get into my ideas on how I plan to implement this yet, but my goal is to make a program that takes peasant.py and generates a corresponding scoring table (I plan on using sqlite3) that gives the expected score for every possible state string. (Which is to say, every state string that can come of following choices from the beginning of the game book; you're not going to have a state string where your STAMINA is, say, 24 because that can't happen in this game book.) To get those state strings, it's going to have to be able to walk through the game book section by section; it can then score those state strings by working backwards from the winning and losing sections.

Once I have that table, I can then ask 'okay, if I'm on this section, and I have these stats and these things, what are my choices, and what's my expected score for each one?' And I can have a third program take peasant.py and that scoring table (or possibly embed all of peasant.py's output in the scoring table, though that would make it much larger) and kick out neatly-formatted BBcode for the given situation. So after I've finished showing off all of the content of the book in my LP, I can then go through and show the optimal path and the expected score at each step, so they can see just how much a good roll makes things better and a bad roll makes things worse.

Again, that's the end goal. (And I did say as much earlier, but maybe I wasn't clear enough?) For now, I just want to implement getting from one state string to another without making Baby Guido cry.

This also works for solving the book, assuming you're passing around a game state. To play the game, you take random rolls. To solve it, you iterate over all possible roll values. The overall code structure is the same: functions calling other functions depending on the values of local variables.

For instance this:
Python code:
def tickle_elephant(args):
    # The final chapter of the book
    for roll in range(1, 7):
        winning_roll = roll > 3
        if winning_roll:
            # Do something
        else:
            # Do something else
is a function that explicitly explores all of its possible outcomes (e.g. we're setting "roll" to each unique possible value of a dice roll and then doing something). You can update the state object (which would be an input arg) in each iteration of the for loop using whatever the roll value is. This is already the solving form of this function. The playable form of this function would need to replace the for loop with a random number generator.

quote:

So would the thing with return vars().get(section, lambda a: None)(state_string) work as a way to call a function by the name in the section string and return its output, or return None if there's no function by that name?

I don't think that this does what you want it to do, and it'd be bad code even if it did work. Don't do this. If you absolutely need to be able to map function string names to functions, create a dict that contains those mappings and then refer to that.

It'd be better still if you didn't need to refer to a lookup table of function names at all and instead just called the functions directly.

QuarkJets fucked around with this message at 22:09 on Oct 1, 2022

QuarkJets
Sep 8, 2008

If a function ever reaches an end-state for the game, call a function that writes off the state to your sqlite database or whatever. Eventually the database will be populated with all of those end states and the paths you took through them.

FredMSloniker
Jan 2, 2008

Why, yes, I do like Kirby games.
On review, I see what you're doing there, QuarkJets. That'd do a depth-first search of the state space, if I'm reading it correctly? There'd be a win() and a lose() to write their scores into the score table, and every other state function would be 'first, call the state functions after this (which will record their scores); then use those scores to calculate the score of this state and record that', right?

...I find myself in something of an awkward situation. On the one hand, I have what I consider to be perfectly valid reasons to not want to do it that way. On the other hand, you're being helpful, and you know more about Python than I do, and I don't want to be all 'well actually I think I should do it differently because' and coming off sounding ungrateful or, worse, like I think I know more than you do. So I'm going to think about this carefully, explain why I think your approach isn't the one I want, and hope you'll understand my intent and respond in kind.

It's entirely possible, in a Fighting Fantasy book, to wind up right back where you were before - not just the same numbered section, but the exact same situation. The most obvious way this can happen is in combat, if all parties involved miss, but there are other ways to get into a loop, some of which aren't escapable (or are only escapable by death). So you wind up in the situation where a state's score x is 1/6 * 1 + 3/6 * 0 + 2/6 * x. A depth-first search is gonna get stuck.

There are a couple of ways I can solve this. I can very carefully code the game so you can't actually loop. I have to be careful I don't screw up the relative probabilities of wins and losses by making draws impossible, of course, and I have to make sure I catch every possible way to loop. I can have some form of loop detection back it out of loops, but it needs to be able to distinguish between loops it can't escape (which should be scored as losses) and loops it can escape (which should be treated as if they don't even exist). I'll definitely want to do the first as much as I can so the solver doesn't waste its time, and the second is a good fallback for if I miss something.

But there's a third option. If I do a breadth-first search, then I can repeatedly iterate over the scores, staring them as a minimum and maximum possible value. In the example case, x would start at [0, 1]. After one iteration, it'd be [1/6, 3/6]. After two, it'd be [8/36, 12/36]. Eventually it'd converge on [1/4, 1/4] - or the closest representations of those values a float can hold - at which point it won't iterate on that score any more. When it can't make any changes, every state should have converged; if not, there's a bug to gish somewhere.

In order to do a breadth-first search, though, I need the state functions to return the results of a single step. Unless I'm missing something. And I need some way to look at the state string and say 'okay, for this section I need to use this function'.

...maybe, instead of having a section string, I could store the next section function in the state itself? Be like

Python code:
def o_and_ngs(state_string):
    """I'm always going to start by assigning these two things to o and next_game_state, so save some typing."""
    return ({"is_random": False, "choice_keys": {}}, loads(state_string)})

def start_of_book(state_string):
    o, next_game_state = o_and_ngs(state_string)
    o["is_random"] = True
    o["text"] = (
        f"You are about to take the lead role in an adventure that will make you a quite generally respected "
        f"person, at least in your neck of the woods. Before you take part in this quest, you must first "
        f"determine your own strengths and weaknesses. You use dice to work out your initial scores. On page 11 "
        f"is an {italic('Adventure Sheet,')} which you may use to record details of your adventure. On it, you "
        f"will find boxes for recording your SKILL, STAMINA and LUCK scores, as well as other details. You are "
        f"advised either to record your scores on the {italic('Adventure Sheet')} in pencil or to make "
        f"photocopies of the sheet for use in future adventures. Take note that you begin this adventure as a "
        f"fifteen-year old Human and your SKILL and STAMINA score are generated a little differently to other "
        f"Fighting Fantasy gamebooks.\n\n{bold('SKILL, STAMINA and LUCK')}\n\nRoll one die."
    )
    next_game_state["section_function"] = roll_stamina  # does it matter, in Python, which order I declare functions in?
    for i in range(6, 9):
        next_game_state["Initial SKILL"], next_game_state["SKILL"] = i, i
        add_choice(o["choice_keys"], (
            f"{bold(f'You rolled a {i * 2 - 11} or a {i * 2 - 10}.')} "
            f"Your {italic('Initial')} SKILL score is {bold(f'{i}.')}"
        ), next_game_state)
    return o

# roll_stamina(state_string), roll_luck(state_string), and all the rest go here

def get_state_complex(state_string):
    return loads(state_string).get("section_function", start_of_book)(state_string)

Would that work?

Falcon2001
Oct 10, 2004

Eat your hamburgers, Apollo.
Pillbug

FredMSloniker posted:

Notepad++. Really, I don't need the ruler now, since I adjusted the window width, but I haven't had a reason to take it out.

You'll have to convince me to learn Visual Studio Code. I'm not saying I can't be convinced, just that the screenshots aren't selling me.

Just to be clear: There's no right answer for how you want to write your code. If you want to use notepad, go for it; I know professional software devs who exclusively use vim which is basically a hyperpowered commandline program, and I know others who swear by highly specialized heavy IDEs like Visual Studio (not-Code)

(Minor side note: VS Code and Visual Studio are entirely different programs and only really share 'you can write code in here!' as a feature. The list of differences is probably bigger than the similarities.)

The biggest argument for VS Code over Notepad++ is that VS Code is highly extensible to do a lot of things, and is purpose built for being a code-aware text editor. It's not a full blown IDE like Visual Studio, it's basically a halfway point between Notepad++ and VS. (Also the extensions platform is much nicer to use than Notepad++ addons.)

Because of that, it bakes in some ideas that are quite handy for programming - here's a few basic ones:
  • Integrated terminal that you can bring up / hide quickly. (ctrl+`)
  • Integration with things like syntax checking / linting to see if your code stands up to best practices.
  • The concept of 'workspaces' which basically means 'here's a collection of folders your code cares about, and we don't have to care about anything higher.
  • Code auto-suggest and documentation on highlight - for example, if I'm writing a python program and I import loads from json, when I type loads() it'll bring up the docstring from the library telling me what arguments it expects and any documentation built in.

Basically it's a lot of little improvements because it was built to write code first and foremost, instead of just being a great notepad replacement. (FWIW: I love Notepad++ and use it a ton.)

Adbot
ADBOT LOVES YOU

Bad Munki
Nov 4, 2008

We're all mad here.


We're trying to use remotezip to access some files via an authenticated session. For various reasons, we've subclassed Session to support some extra things, such as dealing with how requests handles redirects & headers these days, dealing with session headers when accessing files through cloudfront vs. in-region directly through s3, etc. All of those behaviors are non-negotiable, so we're stuck with 'em.

The proof of concept for doing this just creates the RemoteZip object by copying the headers and cookies from the authenticated session, which works fine in the base case, but that loses all the extra features of our custom session. It looks like remotezip just calls requests.get() for most of its actions. What might be the most sensible way to get remotezip to perform all its requests-based actions through our Session object?

I probably explained that horribly...

Here's how we got it working in the most basic sense, which only works in some cases:
Python code:
with RemoteZip(some_url, cookies=special_session.cookies, headers=special_session.headers) as z:
Here, I believe, is where we need remotezip to behave differently:
Python code:
res = requests.get(url, stream=True, **kwargs)
https://github.com/gtsystem/python-remotezip/blob/master/remotezip.py#L179

What I think I want is for remotezip to behave as:
Python code:
res = special_session.get(url, stream=True, **kwargs)
Recommendations?

Bad Munki fucked around with this message at 21:57 on Oct 4, 2022

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply