• This is a new section being rolled out to attract people interested in exploring the origins of the universe and the earth from a biblical perspective. Debate is encouraged and opposing viewpoints are welcome to post but certain rules must be followed. 1. No abusive tagging - if abusive tags are found - they will be deleted and disabled by the Admin team 2. No calling the biblical accounts a fable - fairy tale ect. This is a Christian site, so members that participate here must be respectful in their disagreement.

Evolve 2025!

Clete

Truth Smacker
Silver Subscriber
Those of you who have a long history with Bob Enyart's ministry will remember a computer program that a friend of his had written that randomized the alphabet and checked to see how many letters ended up in the correct locations. I asked Chat GPT to create a program that does the same thing and it totally works!

As I type this, I've had the program running for less than 20 minutes and it has made over 175 million attempts with the best match so far getting 12 letters in the correct location.

I just have the free version of Chat GPT and have hit the limit on my usage on the best version and so tomorrow I'm going to go in and have it display a running total of how much time the program has been running and will post the code for that updated version when once it's working properly.

Also, it's set right now to run 10,000 iterations per cycle. I'm going to have it rewrite the code to that it can take full advantage of my CPU which has 12 cores and 24 threads. That should sent the iterations per second through the roof!

For now, I've posted the python code for the current version. If you want to run it yourself, copy/paste that code into notepad and then save the file as "Evolve_2025.py" Then download Python and install it. When you do the install, make sure to check the box that says “Add Python to PATH” before clicking install. Then, once it's installed, just double click on the Evolve_2025.py file and it should run.

By the way, it's now up to 268 million iterations with 12 correct as the best so far!

The code below is now the latest version from post #21. When you run it, it will ask you how many processes to run at once. Choose a number at or below half of however many threads your CPU has and then experiment with it until you find the optimal number. My system has twelve cores with 24 threads and the optimal number of processes seems to be 8 for my system.

Code:
import curses
import multiprocessing
import random
import time
import json
import os
import sys

# -------------------------------
# Configuration and Constants
# -------------------------------
TARGET = "abcdefghijklmnopqrstuvwxyz"  # Target alphabet
ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyz"  # Allowed characters
PERSISTENT_FILE = "overall_stats.json"       # File for persistent stats

MIN_IPC = 2000  # Minimum iterations per cycle (never drop below this)

# -------------------------------
# Persistence Functions
# -------------------------------
def load_overall_stats():
    default_data = {
        "overall_attempts": 0,
        "overall_elapsed": 0.0,
        "best_data": {"attempt": " " * len(TARGET), "match_count": 0},
        "score_distribution": [0] * (len(TARGET) + 1)
    }
    if os.path.exists(PERSISTENT_FILE):
        try:
            with open(PERSISTENT_FILE, "r") as f:
                data = json.load(f)
            # Ensure all keys are present
            for key, default in default_data.items():
                if key not in data:
                    data[key] = default
        except Exception:
            data = default_data
    else:
        data = default_data
    return data

def save_overall_stats(overall_attempts, overall_elapsed, best_data, score_distribution):
    data = {
        "overall_attempts": overall_attempts,
        "overall_elapsed": overall_elapsed,
        "best_data": dict(best_data),
        "score_distribution": list(score_distribution)
    }
    try:
        with open(PERSISTENT_FILE, "w") as f:
            json.dump(data, f)
    except Exception as e:
        sys.stderr.write(f"Error saving persistent data: {e}\n")

# -------------------------------
# Utility: Time Formatting
# -------------------------------
def format_time(sec):
    total_seconds = sec
    years = int(total_seconds // (365 * 24 * 3600))
    total_seconds %= (365 * 24 * 3600)
    days = int(total_seconds // (24 * 3600))
    total_seconds %= (24 * 3600)
    hours = int(total_seconds // 3600)
    total_seconds %= 3600
    minutes = int(total_seconds // 60)
    seconds = total_seconds % 60
    return f"{years} years, {days} days, {hours} hours, {minutes} minutes, {seconds:05.2f} seconds"

# -------------------------------
# Worker Process Function
# -------------------------------
def worker(p_target, session_attempts, best_data, paused, exit_event,
           score_distribution, distribution_lock, iterations_list, worker_index):
    iterations_per_cycle = 2000  # start at 2000 iterations per cycle
    target_length = len(p_target)
    local_best_match = best_data.get("match_count", 0)
    rnd = random.Random()

    # Variables for sliding-window TPS measurement (window length ~1 second)
    window_start = time.time()
    window_attempts = 0
    last_window_tps = None

    while not exit_event.is_set():
        if paused.value:
            time.sleep(0.1)
            continue

        start_cycle = time.time()
        local_batch_attempts = 0
        local_best_count = local_best_match
        # Local distribution count for this cycle (for scores 0..target_length)
        local_distribution = [0] * (target_length + 1)

        for _ in range(iterations_per_cycle):
            attempt = ''.join(rnd.choice(ALLOWED_CHARS) for _ in range(target_length))
            match_count = sum(1 for i in range(target_length) if attempt[i] == p_target[i])
            local_distribution[match_count] += 1
            local_batch_attempts += 1
            if match_count > local_best_count:
                local_best_count = match_count
                # Update global best if this is an improvement
                if local_best_count > best_data.get("match_count", 0):
                    best_data["attempt"] = attempt
                    best_data["match_count"] = local_best_count

        # Update total session attempts
        with session_attempts.get_lock():
            session_attempts.value += local_batch_attempts

        # Merge this cycle's distribution counts into the shared distribution
        with distribution_lock:
            for i in range(len(local_distribution)):
                # Ensure the list is long enough (it normally is)
                if i < len(score_distribution):
                    score_distribution[i] += local_distribution[i]
                else:
                    score_distribution.append(local_distribution[i])

        # --- New Sliding-Window TPS-Based IPC Adjustment ---
        window_attempts += local_batch_attempts
        current_time = time.time()
        window_duration = current_time - window_start

        if window_duration >= 1.0:
            # Compute instantaneous TPS for this window
            current_window_tps = window_attempts / window_duration

            if last_window_tps is not None:
                # If TPS increased by >5%, increase iterations by 10%
                if current_window_tps > last_window_tps * 1.05:
                    iterations_per_cycle = int(iterations_per_cycle * 1.1)
                # If TPS dropped by >5%, decrease iterations by 10%
                elif current_window_tps < last_window_tps * 0.95:
                    iterations_per_cycle = max(MIN_IPC, int(iterations_per_cycle * 0.9))
            last_window_tps = current_window_tps
            window_start = current_time
            window_attempts = 0

        iterations_list[worker_index] = iterations_per_cycle
        local_best_match = local_best_count

# -------------------------------
# Curses UI Main Function
# -------------------------------
def main(stdscr):
    global NUM_WORKERS

    # Curses initialization
    curses.curs_set(0)
    stdscr.nodelay(True)
    stdscr.keypad(True)
    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(1, curses.COLOR_GREEN, -1)

    # Load persistent stats (including best_data and score_distribution)
    persistent_data = load_overall_stats()
    overall_attempts_loaded = persistent_data.get("overall_attempts", 0)
    overall_elapsed_loaded = persistent_data.get("overall_elapsed", 0.0)
    persistent_best = persistent_data.get("best_data", {"attempt": " " * len(TARGET), "match_count": 0})
    persistent_distribution = persistent_data.get("score_distribution", [0] * (len(TARGET) + 1))

    # Variables for tracking active (non-paused) runtime
    session_active_time = 0.0
    last_loop_time = time.time()

    manager = multiprocessing.Manager()
    # Initialize best_data and score_distribution with persistent values
    best_data = manager.dict(persistent_best)
    session_attempts = multiprocessing.Value('L', 0)
    paused = multiprocessing.Value('b', False)
    exit_event = multiprocessing.Event()

    score_distribution = manager.list(persistent_distribution)
    distribution_lock = multiprocessing.Lock()
    iterations_list = manager.list([2000] * NUM_WORKERS)

    workers = []
    for i in range(NUM_WORKERS):
        p = multiprocessing.Process(target=worker, args=(
            TARGET, session_attempts, best_data, paused, exit_event,
            score_distribution, distribution_lock, iterations_list, i
        ))
        p.start()
        workers.append(p)

    try:
        while True:
            stdscr.clear()
            current_time = time.time()
            dt = current_time - last_loop_time
            if not paused.value:
                session_active_time += dt
            last_loop_time = current_time

            # Handle key presses
            key = stdscr.getch()
            if key != -1:
                if key == 16:  # Ctrl+P toggles pause/resume
                    with paused.get_lock():
                        paused.value = not paused.value
                elif key == 17:  # Ctrl+Q quits the program
                    break

            session_elapsed = session_active_time
            overall_elapsed = overall_elapsed_loaded + session_elapsed

            with session_attempts.get_lock():
                session_attempts_val = session_attempts.value
            total_attempts = overall_attempts_loaded + session_attempts_val

            session_tps = session_attempts_val / session_elapsed if session_elapsed > 0 else 0
            overall_tps = total_attempts / overall_elapsed if overall_elapsed > 0 else 0

            avg_iterations = int(sum(iterations_list) / len(iterations_list)) if len(iterations_list) > 0 else 0

            # Build the display text:
            line = 0
            stdscr.addstr(line, 0, "Target Alphabet:")
            line += 1
            stdscr.addstr(line, 0, TARGET)
            line += 2

            stdscr.addstr(line, 0, "New Best Match:")
            line += 1
            best_attempt = best_data.get("attempt", " " * len(TARGET))
            for i, ch in enumerate(best_attempt):
                if i < len(TARGET) and ch == TARGET[i]:
                    stdscr.addstr(line, i, ch.upper(), curses.color_pair(1))
                else:
                    stdscr.addstr(line, i, ch)
            line += 2

            stdscr.addstr(line, 0, f"Total Attempts: {total_attempts:,}")
            line += 2

            stdscr.addstr(line, 0, f"Session Elapsed Time: {format_time(session_elapsed)}")
            line += 1
            stdscr.addstr(line, 0, f"Overall Elapsed Time: {format_time(overall_elapsed)}")
            line += 2

            stdscr.addstr(line, 0, f"Session Tries per Second: {session_tps:,.2f}")
            line += 1
            stdscr.addstr(line, 0, f"Overall Tries per Second: {overall_tps:,.2f}")
            line += 2

            stdscr.addstr(line, 0, "Score Distribution:")
            line += 1
            best_match = best_data.get("match_count", 0)
            for score in range(best_match + 1):
                stdscr.addstr(line, 0, f"{score} correct: {score_distribution[score]:,}")
                line += 1
            line += 1

            stdscr.addstr(line, 0, f"Number of Processes Running: {NUM_WORKERS}")
            line += 1
            stdscr.addstr(line, 0, f"Iterations per Cycle (avg): {avg_iterations}")
            line += 2

            status_str = "PAUSED" if paused.value else "RUNNING"
            stdscr.addstr(line, 0, f"Status: {status_str} (Pause/Resume: Ctrl+P, Quit: Ctrl+Q)")
            stdscr.refresh()
            time.sleep(0.1)
    finally:
        exit_event.set()
        for p in workers:
            p.join(timeout=1)
        overall_attempts_new = overall_attempts_loaded + session_attempts.value
        overall_elapsed_new = overall_elapsed_loaded + session_active_time
        save_overall_stats(overall_attempts_new, overall_elapsed_new, best_data, score_distribution)

# -------------------------------
# Main Entrypoint
# -------------------------------
if __name__ == '__main__':
    multiprocessing.freeze_support()  # For Windows support
    try:
        num_processes_input = input("Enter number of processes to run: ")
        try:
            num_processes = int(num_processes_input)
            if num_processes < 1:
                raise ValueError
        except ValueError:
            num_processes = min(24, multiprocessing.cpu_count())
            print(f"Invalid input. Defaulting to {num_processes} processes.")
        global NUM_WORKERS
        NUM_WORKERS = num_processes
        curses.wrapper(main)
    except KeyboardInterrupt:
        pass
 
Last edited:

Clete

Truth Smacker
Silver Subscriber
So I just found the following article on KGOV.com!

Did Life Evolve?


Turns out my version is WAY easier than the old one because I have it just shuffling the letters of the alphabet where it can only use each letter one time. If I'm reading it correctly, Bob's version had it where every letter position in the 26 character string can be any one of the 26 letters of the alphabet. So, instead of there being 26! possible combinations, there are 26 to the 26th power (26^26) combinations which is about 15.27 billion times bigger of a number!

I'll make that change tomorrow as well and I'm also going to have it make a way to pause/restart the program without closing it down completely.
 

JudgeRightly

裁判官が正しく判断する
Staff member
Administrator
Super Moderator
Gold Subscriber
Yes the formatting doesn't quite survive the copy/paste process into the post.

BBCode allows for posting code.

Use the [ CODE ] and [ /CODE ] (without spaces) to post code.

There's more, but you're better off seeing what you can do on the BBCode help page:
 

Clete

Truth Smacker
Silver Subscriber
Okay! I've got an upgraded version!

When you run this version, it will ask you how many processes to run. If you have a CPU with multiple cores, it can handle the program running several processes in parallel. This dramatically increases the total number of tries per second.

It will also ask you how many iterations to run per cycle. You'll want to experiment with this number to see what number works best for your system.

My system is pretty nice. My CPU has 12 cores with 24 threads and for my system, the best settings I've found so far is 8 processes at 2000 iterations per cycle. This achieves just over 800,000 attempts per second. It took 20 min and 41 seconds to run 1,000,408,000 attempts!

I also made it display the letters which are in the correct position in green to make them stand out better and I added the ability to pause the program by hitting the space bar.

Lastly, the program creates a file to save the data in so that if you close it you don't lose your progress and when you restart the program it starts from where you left off.

The only bug left is that it sometimes leaves the window open when you stop the program. It's intermittent though so I've not been able to figure out what the problem is.

Here's the code.....

Code:
import random
import time
import json
import os
from collections import defaultdict
from multiprocessing import Pool, cpu_count

def highlight_correct_letters(reference, attempt):
    return ''.join([f"\033[92m{letter.upper()}\033[0m" if letter == reference[i] else letter for i, letter in enumerate(attempt)])

def load_data():
    if os.path.exists("data.json"):
        with open("data.json", "r") as file:
            return json.load(file)
    return {"best_match": "", "best_score": 0, "score_counts": defaultdict(int), "total_attempts": 0, "total_elapsed_time": 0}

def save_data(data):
    with open("data.json", "w") as file:
        json.dump(data, file)

def format_time(seconds):
    years, seconds = divmod(seconds, 31536000)
    days, seconds = divmod(seconds, 86400)
    hours, seconds = divmod(seconds, 3600)
    minutes, seconds = divmod(seconds, 60)
    return f"{years} years, {days} days, {hours} hours, {minutes} minutes, {seconds:.2f} seconds"

def run_iteration(alphabet):
    try:
        random_string = ''.join(random.choice(alphabet) for _ in range(len(alphabet)))
        correct_count = sum(1 for i in range(len(alphabet)) if random_string[i] == alphabet[i])
        return random_string, correct_count
    except KeyboardInterrupt:
        return None, None

def display_data(data, alphabet, start_time, total_paused_time, num_processes, iterations_per_cycle):
    elapsed_time = time.time() - start_time - total_paused_time
    total_elapsed_time = data["total_elapsed_time"] + elapsed_time
    tries_per_second = data["total_attempts"] / total_elapsed_time if total_elapsed_time > 0 else 0
    print("\033c", end="")  # Clears the screen
    print("Target Alphabet:")
    print(alphabet)
    print("\nNew Best Match:")
    print(highlight_correct_letters(alphabet, data["best_match"]))
    print("\nTotal Attempts:", f"{data['total_attempts']:,}")
    print("\nElapsed Time:", format_time(total_elapsed_time))
    print("\nTries per Second:", f"{tries_per_second:,.2f}")
    print("\nScore Distribution:")
    for i in range(27):
        if str(i) in data["score_counts"]:
            print(f"{i} correct: {data['score_counts'][str(i)]:,}")
    print("\nNumber of Processes Running:", num_processes)
    print(f"Iterations per Cycle: {iterations_per_cycle}")
    print("\nInstructions:")
    print("Press 'Space' to pause/resume.")
    print("Press 'Ctrl+C' to stop.")

def main():
    alphabet = "abcdefghijklmnopqrstuvwxyz"
    data = load_data()
   
    print("Target Alphabet:")
    print(alphabet)
    print("\nStarting randomization...\n")
   
    try:
        num_processes = int(input(f"Enter the number of processes to use (1-{cpu_count()}): "))
        if num_processes < 1 or num_processes > cpu_count():
            raise ValueError(f"Number of processes must be between 1 and {cpu_count()}.")
       
        iterations_per_cycle = int(input("Enter the number of iterations per cycle: "))
        if iterations_per_cycle < 1:
            raise ValueError("Number of iterations per cycle must be at least 1.")
       
    except ValueError as e:
        print(f"Invalid input: {e}")
        return
   
    start_time = time.time()
    total_paused_time = 0
    pause_start_time = None
   
    try:
        iteration_count = 0
        last_update_time = time.time()
        paused = False
        with Pool(processes=num_processes) as pool:
            while True:
                if os.name == 'nt':  # For Windows
                    import msvcrt
                    if msvcrt.kbhit() and msvcrt.getch() == b' ':
                        paused = not paused
                        if paused:
                            pause_start_time = time.time()
                        else:
                            total_paused_time += time.time() - pause_start_time
                        time.sleep(0.5)  # Debounce delay
               
                if paused:
                    display_data(data, alphabet, start_time, total_paused_time, num_processes, iterations_per_cycle)
                    print("\nPaused. Press space to resume.")
                    while paused:
                        if os.name == 'nt':  # For Windows
                            if msvcrt.kbhit() and msvcrt.getch() == b' ':
                                paused = False
                                total_paused_time += time.time() - pause_start_time
                                time.sleep(0.5)  # Debounce delay
                    continue

                results = pool.starmap(run_iteration, [(alphabet,) for _ in range(iterations_per_cycle)])
                for random_string, correct_count in results:
                    if random_string is None and correct_count is None:
                        continue
                    data["score_counts"][str(correct_count)] = data["score_counts"].get(str(correct_count), 0) + 1
                    data["total_attempts"] += 1
                   
                    if correct_count > data["best_score"]:
                        data["best_score"] = correct_count
                        data["best_match"] = random_string
                   
                    iteration_count += 1
               
                current_time = time.time()
                elapsed_since_last = current_time - last_update_time
                if elapsed_since_last >= 1:  # Update display every second
                    last_update_time = current_time
                   
                    display_data(data, alphabet, start_time, total_paused_time, num_processes, iterations_per_cycle)
                    save_data(data)
   
    except KeyboardInterrupt:
        elapsed_time = time.time() - start_time - total_paused_time
        data["total_elapsed_time"] += elapsed_time
        save_data(data)
        print("\nProcess stopped by user.")
        display_data(data, alphabet, start_time, total_paused_time, num_processes, iterations_per_cycle)
        pool.terminate()
        pool.join()

if __name__ == "__main__":
    main()
 
Last edited:

Clete

Truth Smacker
Silver Subscriber
By the way. I had MS Copilot calculate the following table....

Here's how many more times you should see each number of correct letters compared to the next:

  • It is 0.96 times more likely to get 0 letters in the correct position than 1 letter.
  • It is 2.00 times more likely to get 1 letter in the correct position than 2 letters.
  • It is 3.13 times more likely to get 2 letters in the correct position than 3 letters.
  • It is 4.35 times more likely to get 3 letters in the correct position than 4 letters.
  • It is 5.68 times more likely to get 4 letters in the correct position than 5 letters.
  • It is 7.14 times more likely to get 5 letters in the correct position than 6 letters.
  • It is 8.75 times more likely to get 6 letters in the correct position than 7 letters.
  • It is 10.53 times more likely to get 7 letters in the correct position than 8 letters.
  • It is 12.50 times more likely to get 8 letters in the correct position than 9 letters.
  • It is 14.71 times more likely to get 9 letters in the correct position than 10 letters.
  • It is 17.24 times more likely to get 10 letters in the correct position than 11 letters.
  • It is 20.00 times more likely to get 11 letters in the correct position than 12 letters.
  • It is 23.08 times more likely to get 12 letters in the correct position than 13 letters.
  • It is 26.47 times more likely to get 13 letters in the correct position than 14 letters.
  • It is 30.30 times more likely to get 14 letters in the correct position than 15 letters.
  • It is 34.62 times more likely to get 15 letters in the correct position than 16 letters.
  • It is 39.47 times more likely to get 16 letters in the correct position than 17 letters.
  • It is 44.83 times more likely to get 17 letters in the correct position than 18 letters.
  • It is 50.77 times more likely to get 18 letters in the correct position than 19 letters.
  • It is 57.14 times more likely to get 19 letters in the correct position than 20 letters.
  • It is 64.29 times more likely to get 20 letters in the correct position than 21 letters.
  • It is 72.73 times more likely to get 21 letters in the correct position than 22 letters.
  • It is 82.35 times more likely to get 22 letters in the correct position than 23 letters.
  • It is 93.75 times more likely to get 23 letters in the correct position than 24 letters.
  • It is 107.14 times more likely to get 24 letters in the correct position than 25 letters.
  • It is 123.08 times more likely to get 25 letters in the correct position than 26 letters.
To the best of our knowledge the record of 15 correct still holds and that was after over 60 trillion attempts.

60 trillion x 34.62 = 2,077,200,000,000,000 attempts before expecting to get 16 correct.
2,077,200,000,000,000 / 800,000 iterations per sec = 2,596,500,000 seconds = 82 years 122 Days 2 hours


Yeah, pretty sure evolution is impossible!
 
Last edited:

Right Divider

Body part
I still have no idea what that even is! Hadn't even heard of it before yesterday! :cool:
I'm a retired software engineer that did quite a bit of Python programming over the years. Definitely my favorite language, very productive.

The multiprocessing library allows using all of the cores of a modern microprocessor to speed up the calculations.
 

Clete

Truth Smacker
Silver Subscriber
I'm a retired software engineer that did quite a bit of Python programming over the years. Definitely my favorite language, very productive.

The multiprocessing library allows using all of the cores of a modern microprocessor to speed up the calculations.
I know exactly nothing at all about Python programming other than I'm pretty sure that it's name starts with the letter P!

It sort of feels like AI is going to put programmers out of business.
 

Clete

Truth Smacker
Silver Subscriber
I'm a retired software engineer that did quite a bit of Python programming over the years. Definitely my favorite language, very productive.

The multiprocessing library allows using all of the cores of a modern microprocessor to speed up the calculations.
Even as fast as this is running, it is still only using about 30% of my CPU according to the Performance tab on Task Manager.
 

Clete

Truth Smacker
Silver Subscriber
So, I'm working on adding some features to the Evolve program and that work has shown me why computer programmers don't need to worry too much about losing their jobs just yet. I'm relying entirely on AI to do the coding on this and every time I add a feature, it creates bugs. I get one bug fixed and it creates a second bug and when I get that bug fixed the first bug reappears and there are some bugs that AI just cannot seem to figure out at all.

So, it's slow going but I'm close to having it just the way I want it.

Stay tuned.
 

JudgeRightly

裁判官が正しく判断する
Staff member
Administrator
Super Moderator
Gold Subscriber
So, I'm working on adding some features to the Evolve program and that work has shown me why computer programmers don't need to worry too much about losing their jobs just yet. I'm relying entirely on AI to do the coding on this and every time I add a feature, it creates bugs. I get one bug fixed and it creates a second bug and when I get that bug fixed the first bug reappears and there are some bugs that AI just cannot seem to figure out at all.

99 little bugs in the code, 99 bugs in the code...
fix one bug, compile it again...
129 little bugs in the code!
 

Clete

Truth Smacker
Silver Subscriber
So, I watched a video about Chat GPT o3-mini and it turns out that it's WAY better at doing coding than 4o!

It's limited usage though on the free version so I'm not quite all the way to where I want it to be. Very close though! Probably tomorrow!
 

Clete

Truth Smacker
Silver Subscriber
Okay! Here's an updated version of the code that is mostly working the way it should.

When you run it, the first thing it will do is to ask you how many processes you want it to run in parallel. I have no idea what the ideal number is for your system but 8 works great for mine. I often get 800K+ tries per second.

The program is supposed to dynamically alter the iterations per cycle to maximize the tries per second but there is still something not right about the way it does that because once it starts slowing down the iterations, it never seems to speed them back up again. I'm working on getting that fixed but it's a tough one. If anyone has any suggestions for a fix, please feel free to offer whatever insights you have.

Code:
import curses
import multiprocessing
import random
import time
import json
import os
import sys

# -------------------------------
# Configuration and Constants
# -------------------------------
TARGET = "abcdefghijklmnopqrstuvwxyz"  # Target alphabet
ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyz"  # Allowed characters
PERSISTENT_FILE = "overall_stats.json"       # File for persistent stats

# Cycle timing thresholds for dynamic adjustment
TARGET_CYCLE_LOWER = 0.005 
TARGET_CYCLE_UPPER = 0.02 
INITIAL_ITERATIONS_PER_CYCLE = 1000

# -------------------------------
# Persistence Functions
# -------------------------------
def load_overall_stats():
    if os.path.exists(PERSISTENT_FILE):
        try:
            with open(PERSISTENT_FILE, "r") as f:
                data = json.load(f)
        except Exception:
            data = {"overall_attempts": 0, "overall_elapsed": 0.0}
    else:
        data = {"overall_attempts": 0, "overall_elapsed": 0.0}
    return data

def save_overall_stats(overall_attempts, overall_elapsed):
    data = {"overall_attempts": overall_attempts, "overall_elapsed": overall_elapsed}
    try:
        with open(PERSISTENT_FILE, "w") as f:
            json.dump(data, f)
    except Exception as e:
        sys.stderr.write(f"Error saving persistent data: {e}\n")

# -------------------------------
# Utility: Time Formatting
# -------------------------------
def format_time(sec):
    # Convert the seconds into whole numbers for years, days, hours, minutes,
    # and leave seconds as a floating point number with 2 decimals.
    total_seconds = sec
    years = int(total_seconds // (365 * 24 * 3600))
    total_seconds %= (365 * 24 * 3600)
    days = int(total_seconds // (24 * 3600))
    total_seconds %= (24 * 3600)
    hours = int(total_seconds // 3600)
    total_seconds %= 3600
    minutes = int(total_seconds // 60)
    seconds = total_seconds % 60
    return f"{years} years, {days} days, {hours} hours, {minutes} minutes, {seconds:05.2f} seconds"

# -------------------------------
# Worker Process Function
# -------------------------------
def worker(p_target, session_attempts, best_data, paused, exit_event,
           score_distribution, distribution_lock, iterations_list, worker_index):
    iterations_per_cycle = INITIAL_ITERATIONS_PER_CYCLE
    target_length = len(p_target)
    local_best_match = 0
    rnd = random.Random()
   
    while not exit_event.is_set():
        if paused.value:
            time.sleep(0.1)
            continue

        start_cycle = time.time()
        local_batch_attempts = 0
        local_best_count = local_best_match
        # Local distribution count for this cycle (for scores 0..target_length)
        local_distribution = [0] * (target_length + 1)

        for _ in range(iterations_per_cycle):
            attempt = ''.join(rnd.choice(ALLOWED_CHARS) for _ in range(target_length))
            match_count = sum(1 for i in range(target_length) if attempt[i] == p_target[i])
            local_distribution[match_count] += 1
            local_batch_attempts += 1
            if match_count > local_best_count:
                local_best_count = match_count
                # Update global best if this is an improvement
                if local_best_count > best_data.get("match_count", 0):
                    best_data["attempt"] = attempt
                    best_data["match_count"] = local_best_count

        # Update total session attempts
        with session_attempts.get_lock():
            session_attempts.value += local_batch_attempts

        # Merge this cycle's distribution counts into the shared distribution
        with distribution_lock:
            for i in range(len(local_distribution)):
                score_distribution[i] += local_distribution[i]

        cycle_duration = time.time() - start_cycle
        if cycle_duration < TARGET_CYCLE_LOWER:
            iterations_per_cycle *= 2
        elif cycle_duration > TARGET_CYCLE_UPPER:
            iterations_per_cycle = max(1, iterations_per_cycle // 2)

        local_best_match = local_best_count
        # Report the current iterations_per_cycle in the shared list
        iterations_list[worker_index] = iterations_per_cycle

# -------------------------------
# Curses UI Main Function
# -------------------------------
def main(stdscr):
    global NUM_WORKERS  # Set before calling curses.wrapper

    # Curses initialization
    curses.curs_set(0)
    stdscr.nodelay(True)
    stdscr.keypad(True)
    curses.start_color()
    curses.use_default_colors()
    curses.init_pair(1, curses.COLOR_GREEN, -1)  # For correct letters in best match

    overall_stats = load_overall_stats()
    overall_attempts_loaded = overall_stats.get("overall_attempts", 0)
    overall_elapsed_loaded = overall_stats.get("overall_elapsed", 0.0)

    # Variables for tracking active (non-paused) runtime
    session_active_time = 0.0
    last_loop_time = time.time()

    manager = multiprocessing.Manager()
    best_data = manager.dict()
    best_data["attempt"] = " " * len(TARGET)
    best_data["match_count"] = 0

    session_attempts = multiprocessing.Value('L', 0)
    paused = multiprocessing.Value('b', False)
    exit_event = multiprocessing.Event()

    # Shared score distribution array for scores 0 to len(TARGET)
    score_distribution = manager.list([0] * (len(TARGET) + 1))
    distribution_lock = multiprocessing.Lock()
    # Shared list for each worker’s current iterations per cycle
    iterations_list = manager.list([INITIAL_ITERATIONS_PER_CYCLE] * NUM_WORKERS)

    workers = []
    for i in range(NUM_WORKERS):
        p = multiprocessing.Process(target=worker, args=(
            TARGET, session_attempts, best_data, paused, exit_event,
            score_distribution, distribution_lock, iterations_list, i
        ))
        p.start()
        workers.append(p)

    try:
        while True:
            stdscr.clear()
            current_time = time.time()
            dt = current_time - last_loop_time
            if not paused.value:
                session_active_time += dt
            last_loop_time = current_time

            # Handle key presses
            key = stdscr.getch()
            if key != -1:
                if key == 16:  # Ctrl+P toggles pause/resume
                    with paused.get_lock():
                        paused.value = not paused.value
                elif key == 17:  # Ctrl+Q quits the program
                    break

            session_elapsed = session_active_time
            overall_elapsed = overall_elapsed_loaded + session_elapsed

            with session_attempts.get_lock():
                session_attempts_val = session_attempts.value
            total_attempts = overall_attempts_loaded + session_attempts_val

            session_tps = session_attempts_val / session_elapsed if session_elapsed > 0 else 0
            overall_tps = total_attempts / overall_elapsed if overall_elapsed > 0 else 0

            # Compute average iterations per cycle across all workers
            avg_iterations = int(sum(iterations_list) / len(iterations_list)) if len(iterations_list) > 0 else 0

            # Build the display text:
            line = 0
            stdscr.addstr(line, 0, "Target Alphabet:")
            line += 1
            stdscr.addstr(line, 0, TARGET)
            line += 2

            stdscr.addstr(line, 0, "New Best Match:")
            line += 1
            best_attempt = best_data.get("attempt", " " * len(TARGET))
            # Display each letter: if it matches the target at that position, show it as an uppercase letter in green
            for i, ch in enumerate(best_attempt):
                if i < len(TARGET) and ch == TARGET[i]:
                    stdscr.addstr(line, i, ch.upper(), curses.color_pair(1))
                else:
                    stdscr.addstr(line, i, ch)
            line += 2

            stdscr.addstr(line, 0, f"Total Attempts: {total_attempts:,}")
            line += 2

            stdscr.addstr(line, 0, f"Session Elapsed Time: {format_time(session_elapsed)}")
            line += 1
            stdscr.addstr(line, 0, f"Overall Elapsed Time: {format_time(overall_elapsed)}")
            line += 2

            stdscr.addstr(line, 0, f"Session Tries per Second: {session_tps:,.2f}")
            line += 1
            stdscr.addstr(line, 0, f"Overall Tries per Second: {overall_tps:,.2f}")
            line += 2

            stdscr.addstr(line, 0, "Score Distribution:")
            line += 1
            best_match = best_data.get("match_count", 0)
            # Display counts from 0 correct up to best match (inclusive)
            for score in range(best_match + 1):
                stdscr.addstr(line, 0, f"{score} correct: {score_distribution[score]:,}")
                line += 1
            line += 1

            stdscr.addstr(line, 0, f"Number of Processes Running: {NUM_WORKERS}")
            line += 1
            stdscr.addstr(line, 0, f"Iterations per Cycle: {avg_iterations}")
            line += 2

            status_str = "PAUSED" if paused.value else "RUNNING"
            stdscr.addstr(line, 0, f"Status: {status_str} (Pause/Resume: Ctrl+P, Quit: Ctrl+Q)")
            stdscr.refresh()
            time.sleep(0.1)
    finally:
        exit_event.set()
        for p in workers:
            p.join(timeout=1)
        overall_attempts_new = overall_attempts_loaded + session_attempts.value
        overall_elapsed_new = overall_elapsed_loaded + session_active_time
        save_overall_stats(overall_attempts_new, overall_elapsed_new)

# -------------------------------
# Main Entrypoint
# -------------------------------
if __name__ == '__main__':
    multiprocessing.freeze_support()  # For Windows support
    try:
        num_processes_input = input("Enter number of processes to run: ")
        try:
            num_processes = int(num_processes_input)
            if num_processes < 1:
                raise ValueError
        except ValueError:
            num_processes = min(24, multiprocessing.cpu_count())
            print(f"Invalid input. Defaulting to {num_processes} processes.")
        global NUM_WORKERS
        NUM_WORKERS = num_processes
        curses.wrapper(main)
    except KeyboardInterrupt:
        pass
 
Top