DNA + Conway's Game of Life - Stochastic Computer

 




Introduction

DNA Life Simulation is a small experimental program exploring the idea of DNA as a generative program rather than simply a container of information.


DNA and the Cell as a Stochastic Computer

One way to think about a living cell is as a fundamentally different kind of computer from the computers we build.

In a conventional computer, a program is executed according to a precisely defined sequence of instructions. The processor reads an instruction from a particular position in memory, executes it, and then normally proceeds to the next instruction—or uses an explicit control-flow instruction such as GOTO, a jump, a branch, or a function call to determine where execution should continue.

A cell can be viewed, in a highly simplified way, as a stochastic computer.

The DNA represents the program—the encoded set of instructions from which the cell can construct and regulate itself. Ribosomes and the other components of the cellular machinery act as part of the interpreter or execution system that turns this information into physical processes, such as the production of proteins.

In the simplified model used by this experiment, a ribosome can be thought of as something resembling the read head of a Turing machine. It occupies a position on the DNA sequence and reads the code from a position marked as the beginning of a gene until it reaches a termination signal.

The crucial difference is that there is no simple equivalent of a GOTO instruction telling the reader exactly which gene should be executed next.

Instead, the state of the environment influences where the cellular machinery proceeds next.

Chemical concentrations, signaling molecules, regulatory proteins, gradients, feedback mechanisms, and many other factors can influence which genes are expressed and when. In our simplified model, these factors are represented by the state of the surrounding environment and act as modifiers of program execution.

This creates a feedback loop:

DNA -> gene expression -> protein production -> changes in the cellular environment -> changes in gene expression -> more protein production -> ...

The program therefore does not simply execute from beginning to end. Its execution continuously changes the conditions under which the program itself is being interpreted.

This is what makes the system fundamentally different from the usual idea of a deterministic program.

From stochastic events to deterministic-looking structures

At the microscopic level, the processes inside a cell are not perfectly deterministic. Molecules move through the cell, interact probabilistically, bind and unbind, and are produced and degraded. Even genetically identical cells can therefore differ in their exact molecular state.

Yet at a higher level, these enormous numbers of stochastic events can produce remarkably reproducible results.

Cells divide.
Tissues develop characteristic structures.
Organs form in reproducible locations and shapes.
An embryo follows a highly organized developmental trajectory.

Perhaps one of the most striking examples is identical twins. They begin with essentially the same genetic sequence and, initially, very similar cellular machinery. Their development is nevertheless not molecularly identical: individual cells experience different microscopic events and environmental fluctuations.

And yet, at the macroscopic level, identical twins generally develop an astonishingly similar overall body plan and appearance.

This suggests an important distinction:

The microscopic process can be stochastic while the macroscopic behavior can be highly constrained and reproducible.

The DNA does not have to specify every individual event. Instead, it can encode a system of rules and interactions that guides a huge number of stochastic processes toward a relatively narrow range of possible macroscopic outcomes.

The central idea

From this perspective, DNA is not simply a static description of an organism.

It is more like a program executed by a stochastic biological computer, where the interpreter, the program, and the environment continuously interact.

The fascinating consequence is that a relatively compact genetic program can generate an extraordinarily complex structure without containing an explicit description of every cell, every connection, or every molecule.

That is also the idea this simulation is trying to explore in an extremely simplified form:

What happens if a program does not directly specify its final result, but instead specifies a process whose execution changes the environment that controls the subsequent execution of the program itself?

This creates a system in which code, execution, environment, and resulting structure form one continuous feedback process.


The Central Experimental Question

This leads to a broader question:

Can a stochastic computer be programmed to produce a predefined structure simply by executing a program encoded in its “DNA”?

In other words, instead of explicitly describing the desired final structure, can we construct a relatively small piece of code that, when executed by a stochastic system, causes that structure to emerge?

This is a fundamentally different problem from conventional programming. In a conventional computer, if we want a particular image or structure, we can explicitly specify every pixel or every step required to construct it. In a stochastic system, however, the individual events are not completely predetermined. The challenge is to design the program and its interaction with the environment so that the desired macroscopic result emerges despite microscopic uncertainty.

The application therefore allows the user to modify the simulated DNA directly. By changing individual instructions, adding or removing genes, and observing the resulting behavior, the user can experiment with the question of whether a desired outcome can be programmed indirectly.

The goal is not necessarily to encode the final shape itself, but to discover a sequence of instructions that causes the system to generate the shape as a consequence of its own execution.

This raises an interesting distinction:

Are we programming the result, or are we programming the process that makes the result emerge?

The DNA editor in the application provides a simple way to explore this question experimentally. The user can modify the genetic code, run the stochastic system, clear the resulting world, and observe whether the modified program repeatedly moves the system toward the desired structure.

In the longer term, the experiment could even be extended so that the system itself searches for DNA sequences capable of producing a predefined target. That would turn the experiment from simply executing DNA into an exploration of program synthesis through stochastic development.

Background Idea

The idea behind this experiment is to explore whether a biological system can be viewed as a stochastic computer—a system in which DNA acts as a program, while the cellular machinery acts as its interpreter.

Unlike a conventional computer, where program execution follows an explicitly defined sequence of instructions, execution in a biological system is strongly influenced by the current state of the environment. The expression of one part of the genetic program changes the environment, which in turn influences what is expressed next.

This creates a continuous feedback loop:

DNA → execution → environmental change → modified execution → new environmental change → ...

From this perspective, development can be seen not as the execution of a completely deterministic sequence of instructions, but as a stochastic process constrained by the genetic program.

This leads to the main question behind the experiment:

Can we program a stochastic computer so that, by executing a program encoded in its “DNA”, it eventually produces a predefined structure?

The interesting part is that the desired structure does not have to be explicitly encoded in the program. Instead, the program could encode a process that causes the structure to emerge through interaction with its environment.

The simulation provides a small experimental playground for this idea. The user can modify the simulated DNA, run the system, observe the resulting structures, clear the environment, and modify the program again.

The long-term idea is to investigate whether a relatively simple genetic program can produce increasingly complex and well-defined structures through stochastic execution and environmental feedback, rather than through a direct description of the final result.

The simulation combines three elements: a simplified DNA sequence, a ribosome-like interpreter, and Conway's Game of Life. The DNA contains instructions represented by letters that correspond to different Life patterns, while the simulated ribosome moves through a changing environment and decides what to read based on what it senses.

When a gene is activated, its instructions are interpreted sequentially. Individual symbols can create Life structures such as blocks, gliders, oscillators, spaceships, or more complex patterns. The resulting structures then evolve according to the standard Conway Life rules.

The important idea is that the DNA does not describe the final state of the world. Instead, it describes a process capable of generating structures and behavior. The environment influences which instructions are activated, while the resulting Life world subsequently becomes the new environment.

The program is therefore a deliberately simplified model of a much broader biological concept:

Information + interpreter + environment → emergent structure and behavior.

It is not intended to reproduce real biological processes. Rather, it provides a visual sandbox for exploring questions about generative encoding, information compression, development, stochastic processes, and the relationship between a program and the structure it produces.


How to Use the Application

When the application starts, it automatically generates a random DNA sequence and creates an initial Conway's Game of Life environment. The DNA is displayed horizontally at the top of the window, while the evolving Life world occupies the main part of the screen.

Starting the Simulation

The simulation starts automatically.

A ribosome is represented by a red square moving through the Life world. It periodically measures its local environment using a small sensing area around its current position. The number of living cells in this area is treated as the local temperature.

The temperature determines which gene is selected for execution.

Following the Active Gene

The DNA sequence consists of genes enclosed between:

  • A — Start of a gene
  • Z — End of a gene

When a gene becomes active, the ribosome reads it one character at a time.

The currently read DNA character is highlighted, and recently read characters remain highlighted for a short time with a fading effect. This makes it possible to visually follow the ribosome's progress through the gene.

For example:

A B G I G Z
    ↑
  currently reading

Each letter between A and Z represents an instruction to create a particular Conway Life pattern.

During gene execution, the ribosome moves vertically through the Life world as it creates the corresponding structures. After the gene has been completely read, the ribosome returns to its environmental exploration and eventually selects another gene.

DNA Editor

The DNA sequence can be modified directly from the application.

First, click a character in the DNA sequence. This selects that position.

The available instruction symbols are displayed in the DNA Editor on the right side of the window. Clicking one of them replaces the selected DNA character.

For example, selecting a B position and then clicking G changes B into G.

The available symbols represent different Life structures, such as:

  • B — Block
  • H — Beehive
  • I — Blinker
  • G — Glider
  • P — Pulsar
  • W — Lightweight Spaceship
  • Q — Acorn

A and Z are used to define gene boundaries.

This allows the user to experiment interactively with the DNA while observing how relatively small changes in the sequence affect the resulting behavior.

Clearing the Life World

The CLS — Clear Screen button removes all living cells from the Life world.

Importantly, this does not reset the DNA or the simulation. The ribosome continues from its current state, and the simulation continues running.

This makes CLS useful as an experimental tool. For example, you can allow the system to create a complex structure, clear the screen, and then observe what happens when the same DNA continues operating in an empty environment.

Observing the Experiment

The most interesting way to use the application is to let it run for some time and observe the relationship between:

DNA → gene selection → instruction execution → Life structures → environment → new gene selection

You can then modify individual DNA characters, clear the screen, and observe how these changes influence the subsequent development of the system.

The application is therefore intended less as a conventional game and more as an interactive laboratory for experimenting with generative programs and emergent behavior.


Python code:



import random

import tkinter as tk


# ============================================================

# Configuration

# ============================================================


WORLD_SIZE = 100

SENSOR_SIZE = 10

DNA_LENGTH = 40

UPDATE_MS = 60


# Number of Life iterations to wait before reading another gene.

# This prevents newly inserted patterns from immediately piling up.

GENE_COOLDOWN = 25


# Patterns shown in the legend on the right side of the window.

DISPLAY_PATTERNS = ["B", "H", "I", "G", "P", "W", "Q"]


# DNA highlighting:

# A newly read character starts at this intensity and fades by one

# unit on every simulation iteration.

DNA_GLOW_MAX = 12


# ============================================================

# Life patterns

# ============================================================

# A and Z are reserved as DNA START/STOP markers.

# The other letters encode predefined Conway Life patterns.

#

# These include stable objects, oscillators, and a glider.

# ============================================================


PATTERNS = {

    "B": ["11", "11"],                 # Block - stable

    "H": ["0110", "1001", "0110"],     # Beehive - stable

    "L": ["011", "101", "010"],        # Loaf - stable

    "I": ["111"],                      # Blinker - oscillator

    "O": ["0110", "1001", "1001", "0110"],  # stable 4x4 object

    "C": ["1100", "1100", "0011", "0011"],  # Beacon - oscillator

    "G": ["010", "001", "111"],        # Glider

    "R": ["011", "110", "010"],        # R-pentomino


    # Additional interesting patterns:

    "P": [                              # Pulsar - period 3 oscillator

        "00111100111100",

        "00111100111100",

        "00000011000000",

        "11000110110001",

        "11000110110001",

        "11000110110001",

        "00111100111100",

        "00111100111100",

    ],


    "W": [                              # Lightweight spaceship (LWSS)

        "01110",

        "10001",

        "00001",

        "10010",

    ],


    "Q": [                              # Acorn - long-lived methuselah

        "0100000",

        "0001000",

        "1101100",

        "0000100",

        "0000000",

        "0000000",

        "0000000",

    ],

}



# ============================================================

# DNA

# ============================================================


def create_dna(length=DNA_LENGTH):

    """Create random DNA containing A...Z genes.


    A = START

    Z = STOP


    Example:

        AGBIZ

    means that the gene contains Glider, Block, Blinker, Beehive.

    """


    letters = list(PATTERNS.keys())

    dna = []


    while len(dna) < length:

        dna.append("A")                 # Start a gene


        gene_length = random.randint(1, 5)

        for _ in range(gene_length):

            dna.append(random.choice(letters))


        dna.append("Z")                 # End the gene


    return "".join(dna[:length])



def find_genes(dna):

    """Find all complete A...Z genes in the DNA.


    Returns:

        [(start_index, stop_index, [pattern_letters]), ...]

    """


    genes = []

    i = 0


    while i < len(dna):

        if dna[i] == "A":

            start = i

            i += 1

            gene = []


            while i < len(dna) and dna[i] != "Z":

                if dna[i] in PATTERNS:

                    gene.append(dna[i])

                i += 1


            if i < len(dna) and dna[i] == "Z":

                genes.append((start, i, gene))


        i += 1


    return genes



# ============================================================

# Conway's Game of Life

# ============================================================


def create_random_world():

    """Create the initial 100x100 Life world."""


    return [

        [random.randint(0, 1) for _ in range(WORLD_SIZE)]

        for _ in range(WORLD_SIZE)

    ]



def count_neighbors(world, x, y):

    """Count the eight living neighbors of one cell.


    The world wraps around at the edges, so it behaves like a torus.

    """


    total = 0


    for dy in (-1, 0, 1):

        for dx in (-1, 0, 1):

            if dx == 0 and dy == 0:

                continue


            nx = (x + dx) % WORLD_SIZE

            ny = (y + dy) % WORLD_SIZE


            total += world[ny][nx]


    return total



def next_generation(world):

    """Calculate one complete Conway Life generation."""


    new_world = [

        [0 for _ in range(WORLD_SIZE)]

        for _ in range(WORLD_SIZE)

    ]


    for y in range(WORLD_SIZE):

        for x in range(WORLD_SIZE):

            alive = world[y][x]

            neighbors = count_neighbors(world, x, y)


            if alive and neighbors in (2, 3):

                new_world[y][x] = 1

            elif not alive and neighbors == 3:

                new_world[y][x] = 1


    return new_world



# ============================================================

# Environment sensor

# ============================================================


def measure_temperature(world, x, y):

    """Count living cells in a SENSOR_SIZE x SENSOR_SIZE window.


    This is the ribosome's simple environmental sensor.


    With a 10x10 sensor the value is between 0 and 100.

    The window wraps around the world edges.

    """


    half = SENSOR_SIZE // 2

    total = 0


    for dy in range(-half, half):

        for dx in range(-half, half):

            nx = (x + dx) % WORLD_SIZE

            ny = (y + dy) % WORLD_SIZE

            total += world[ny][nx]


    return total



# ============================================================

# Pattern insertion

# ============================================================


def place_pattern(world, pattern, x, y):

    """Insert a Life pattern into the world at (x, y)."""


    for dy, row in enumerate(pattern):

        for dx, value in enumerate(row):

            if value == "1":

                px = (x + dx) % WORLD_SIZE

                py = (y + dy) % WORLD_SIZE

                world[py][px] = 1



def activate_gene(world, gene, x, y):

    """Execute one gene by inserting its encoded patterns.


    Patterns are placed next to each other starting at the ribosome's

    current position.

    """


    cursor_x = x

    cursor_y = y


    for letter in gene:

        pattern = PATTERNS[letter]

        place_pattern(world, pattern, cursor_x, cursor_y)


        cursor_x += len(pattern[0]) + 1


        if cursor_x >= WORLD_SIZE:

            cursor_x = 0

            cursor_y = (cursor_y + 5) % WORLD_SIZE



# ============================================================

# Ribosome

# ============================================================


class Ribosome:

    """A simple DNA interpreter controlled by its environment."""


    def __init__(self, dna):

        self.dna = dna

        self.genes = find_genes(dna)


        self.x = WORLD_SIZE // 2

        self.y = WORLD_SIZE // 2


        self.temperature = 0

        self.selected_gene = None


        # Countdown between completed gene readings.

        self.gene_cooldown = 0


        # DNA positions belonging to the gene currently being read.

        self.reading_positions = []

        self.reading_cursor = 0


        # A glow counter for every DNA character.

        # 0 = no glow, larger values = stronger glow.

        self.dna_glow = [0] * len(dna)


    def random_walk(self):

        """Move -1, 0, or +1 in both dimensions."""


        self.x = (self.x + random.choice([-1, 0, 1])) % WORLD_SIZE

        self.y = (self.y + random.choice([-1, 0, 1])) % WORLD_SIZE


    def select_gene(self, temperature):

        """Use the environmental measurement to select a gene.


        Temperature 0 selects the first gene, temperature 1 the second,

        etc. If the temperature is larger than the number of genes,

        the last available gene is selected.

        """


        if not self.genes:

            return None


        return min(temperature, len(self.genes) - 1)


    def step(self, world):

        """Perform one ribosome cycle.


        The ribosome reads a selected gene one DNA character at a time.

        Every character gets a temporary visual glow which fades gradually.

        This makes the reading process visible instead of producing a

        single very short flash.

        """


        # The glow fades on every simulation iteration.

        self.dna_glow = [

            max(0, value - 1)

            for value in self.dna_glow

        ]


        # 1. If we are already reading a gene, keep the ribosome

        #    at the current column. Each newly created pattern is

        #    placed one pattern-height lower than the previous one.

        if self.reading_cursor < len(self.reading_positions):


            position = self.reading_positions[self.reading_cursor]


            # Make this DNA character glow.

            self.dna_glow[position] = DNA_GLOW_MAX


            letter = self.dna[position]


            # A and Z are control characters, so they do not create

            # a Life pattern. The other letters do.

            if letter in PATTERNS:

                pattern = PATTERNS[letter]


                place_pattern(

                    world,

                    pattern,

                    self.x,

                    self.y

                )


                # Move down by the height of the pattern plus one

                # empty row. This prevents consecutive patterns from

                # being drawn directly on top of each other.

                self.y = (

                    self.y + len(pattern) + 1

                ) % WORLD_SIZE


            self.reading_cursor += 1

            self.reading_dna_position = position


            # If this was the last character (Z), finish the gene

            # and start the cooldown period.

            if self.reading_cursor >= len(self.reading_positions):

                self.gene_cooldown = GENE_COOLDOWN


            return


        # 2. During cooldown, the ribosome can move randomly again.

        if self.gene_cooldown > 0:

            self.random_walk()


            # Sense the local environment while wandering.

            self.temperature = measure_temperature(

                world, self.x, self.y

            )


            self.gene_cooldown -= 1

            self.selected_gene = None

            self.reading_dna_position = None

            return


        # 3. The ribosome is free again. Move randomly and sense

        #    the new local environment before selecting a gene.

        self.random_walk()


        self.temperature = measure_temperature(

            world, self.x, self.y

        )


        # 4. The ribosome is free: use the environment to select a gene.

        if self.gene_cooldown > 0:

            self.gene_cooldown -= 1

            self.selected_gene = None

            self.reading_dna_position = None

            return


        # 5. The ribosome is free: use the environment to select a gene.

        self.selected_gene = self.select_gene(self.temperature)


        if self.selected_gene is not None:

            start, end, _ = self.genes[self.selected_gene]


            # Read every character from A through Z, one per iteration.

            self.reading_positions = list(range(start, end + 1))

            self.reading_cursor = 0


            # The first character (A) will be read on this iteration.

            position = self.reading_positions[self.reading_cursor]

            self.dna_glow[position] = DNA_GLOW_MAX

            self.reading_dna_position = position

            self.reading_cursor += 1




# ============================================================

# Graphical interface

# ============================================================


class LifeApp:

    """Tkinter display for the Life world, DNA and ribosome."""


    def __init__(self, root, world, ribosome):

        self.root = root

        self.world = world

        self.ribosome = ribosome


        root.title("DNA + Conway's Game of Life")


        # Main layout:

        #

        #   DNA sequence across the top

        #   -----------------------------------------

        #   |                LIFE       | PATTERNS  |

        #   |                WORLD      | B = Block |

        #   |                           | H = ...   |

        #   -----------------------------------------


        self.main_frame = tk.Frame(root)

        self.main_frame.pack()


        # --------------------------------------------------------

        # DNA display

        # --------------------------------------------------------


        self.dna_frame = tk.Frame(self.main_frame)

        self.dna_frame.grid(row=0, column=0, columnspan=2, pady=(8, 5))


        tk.Label(

            self.dna_frame,

            text="DNA:",

            font=("Consolas", 12, "bold")

        ).pack(side=tk.LEFT, padx=(0, 8))


        self.dna_labels = []


        for i, letter in enumerate(ribosome.dna):

            label = tk.Label(

                self.dna_frame,

                text=letter,

                font=("Consolas", 14, "bold"),

                width=2,

                cursor="hand2"

            )

            label.pack(side=tk.LEFT)


            # Clicking a DNA character selects it for editing.

            label.bind(

                "<Button-1>",

                lambda event, index=i: self.select_dna_position(index)

            )


            self.dna_labels.append(label)


        # Index of the DNA character selected by the user.

        self.selected_dna_position = None


        # --------------------------------------------------------

        # Life world

        # --------------------------------------------------------


        self.canvas_size = 600

        self.cell_size = self.canvas_size / WORLD_SIZE


        self.canvas = tk.Canvas(

            self.main_frame,

            width=self.canvas_size,

            height=self.canvas_size,

            bg="black"

        )

        self.canvas.grid(row=1, column=0, padx=(5, 10), pady=5)


        # --------------------------------------------------------

        # Pattern legend

        # --------------------------------------------------------


        self.pattern_frame = tk.Frame(

            self.main_frame,

            relief=tk.GROOVE,

            borderwidth=2,

            padx=8,

            pady=8

        )

        self.pattern_frame.grid(

            row=1,

            column=1,

            sticky="n",

            padx=(0, 5),

            pady=5

        )


        # Header and CLS action share the same horizontal row, so

        # CLS does not consume an additional vertical row.

        header_row = tk.Frame(self.pattern_frame)

        header_row.pack(fill="x", pady=(0, 6))


        tk.Label(

            header_row,

            text="DNA EDITOR",

            font=("Consolas", 12, "bold")

        ).pack(side=tk.LEFT)


        # User action: clear the entire Life world immediately.

        tk.Button(

            header_row,

            text="CLS",

            font=("Consolas", 9, "bold"),

            cursor="hand2",

            command=self.clear_screen

        ).pack(side=tk.RIGHT, padx=(8, 0))


        tk.Label(

            self.pattern_frame,

            text="Click DNA, then click a letter below",

            font=("Consolas", 8)

        ).pack(pady=(0, 8))


        # A and Z are DNA control symbols, not Life patterns.

        self.add_palette_item("A", "START", None)

        self.add_palette_item("Z", "STOP", None)


        # Life patterns available for editing the DNA.

        for letter in DISPLAY_PATTERNS:

            self.add_palette_item(letter, "", PATTERNS[letter])


        # --------------------------------------------------------

        # Status

        # --------------------------------------------------------


        self.info = tk.Label(

            root,

            text="",

            font=("Consolas", 11)

        )

        self.info.pack(pady=(3, 5))


        self.update()


    def add_palette_item(self, letter, description, pattern):

        """Create one clickable item in the DNA editing palette."""


        row = tk.Frame(self.pattern_frame)

        row.pack(anchor="w", pady=4)


        button = tk.Button(

            row,

            text=letter,

            font=("Consolas", 14, "bold"),

            width=3,

            cursor="hand2",

            command=lambda value=letter: self.replace_selected_dna(value)

        )

        button.pack(side=tk.LEFT)


        if pattern is None:

            # A/Z do not have a graphical Life shape.

            tk.Label(

                row,

                text=description,

                font=("Consolas", 9),

                width=12,

                anchor="w"

            ).pack(side=tk.LEFT, padx=(5, 0))

        else:

            # Keep a fixed preview area so the right-hand editor

            # does not become excessively wide. The pattern itself is

            # scaled to fit completely inside this area.

            preview_width = 55

            preview_height = 40


            preview = tk.Canvas(

                row,

                width=preview_width,

                height=preview_height,

                bg="black",

                highlightthickness=1

            )

            preview.pack(side=tk.LEFT, padx=(5, 0))

            self.draw_pattern_preview(preview, pattern)


    def clear_screen(self):

        """Clear the entire Life world without changing the DNA."""


        for y in range(WORLD_SIZE):

            for x in range(WORLD_SIZE):

                self.world[y][x] = 0


        # The ribosome and its DNA-reading state are left untouched.

        # Therefore the experiment continues exactly from this point,

        # but with an empty Life world.

        self.draw_world()


    def select_dna_position(self, index):

        """Select one DNA position for editing."""


        self.selected_dna_position = index

        self.update_dna_display()


    def replace_selected_dna(self, letter):

        """Replace the currently selected DNA character.


        The DNA string is changed immediately. We also rebuild the list

        of genes so the ribosome sees the new DNA structure.

        """


        if self.selected_dna_position is None:

            return


        index = self.selected_dna_position


        # Replace exactly one character.

        dna = list(self.ribosome.dna)

        dna[index] = letter

        self.ribosome.dna = "".join(dna)


        # Rebuild genes because A/Z changes can alter gene boundaries.

        self.ribosome.genes = find_genes(self.ribosome.dna)


        # Update the visible DNA character.

        self.dna_labels[index].config(text=letter)


        self.update_dna_display()


    def draw_pattern_preview(self, canvas, pattern):

        """Draw a small preview of a Life pattern in the DNA editor."""


        canvas.delete("all")


        # Scale the pattern so the complete shape fits in the preview.

        canvas_w = int(canvas.cget("width"))

        canvas_h = int(canvas.cget("height"))


        rows = len(pattern)

        cols = max(len(row) for row in pattern)


        margin = 6

        cell = max(

            2,

            min(

                (canvas_w - 2 * margin) // cols,

                (canvas_h - 2 * margin) // rows

            )

        )


        pattern_w = cols * cell

        pattern_h = rows * cell

        offset_x = (canvas_w - pattern_w) // 2

        offset_y = (canvas_h - pattern_h) // 2


        for y, row in enumerate(pattern):

            for x, value in enumerate(row):

                if value == "1":

                    canvas.create_rectangle(

                        offset_x + x * cell,

                        offset_y + y * cell,

                        offset_x + (x + 1) * cell,

                        offset_y + (y + 1) * cell,

                        fill="white",

                        outline=""

                    )


    def update_dna_display(self):

        """Display DNA selection and a fading glow for recently read bases."""


        selected = self.selected_dna_position


        for i, label in enumerate(self.dna_labels):


            # User selection is shown with a solid blue border.

            # This is deliberately different from the yellow reading glow.

            if i == selected:

                label.config(

                    relief=tk.SOLID,

                    borderwidth=2

                )

            else:

                label.config(

                    relief=tk.FLAT,

                    borderwidth=0

                )


            # Convert glow intensity into a color.

            # High intensity -> yellow.

            # Zero intensity -> normal window background.

            intensity = self.ribosome.dna_glow[i]


            if intensity > 0:

                fraction = intensity / DNA_GLOW_MAX


                # Blend the normal background with yellow.

                bg_rgb = self.root.winfo_rgb(

                    self.root.cget("bg")

                )

                yellow_rgb = (65535, 65535, 0)


                r = int(

                    bg_rgb[0] +

                    (yellow_rgb[0] - bg_rgb[0]) * fraction

                )

                g = int(

                    bg_rgb[1] +

                    (yellow_rgb[1] - bg_rgb[1]) * fraction

                )

                b = int(

                    bg_rgb[2] +

                    (yellow_rgb[2] - bg_rgb[2]) * fraction

                )


                bg = f"#{r // 256:02x}{g // 256:02x}{b // 256:02x}"

            else:

                bg = self.root.cget("bg")


            label.config(

                bg=bg,

                fg="black",

                font=("Consolas", 14, "bold")

            )



    def draw_world(self):

        """Draw living cells and the ribosome."""


        self.canvas.delete("all")

        cell = self.cell_size


        for y in range(WORLD_SIZE):

            for x in range(WORLD_SIZE):

                if self.world[y][x]:

                    self.canvas.create_rectangle(

                        x * cell,

                        y * cell,

                        (x + 1) * cell,

                        (y + 1) * cell,

                        fill="white",

                        outline=""

                    )


        # Red outline = current ribosome position.

        rx = self.ribosome.x * cell

        ry = self.ribosome.y * cell


        self.canvas.create_oval(

            rx,

            ry,

            rx + cell,

            ry + cell,

            outline="red",

            width=2

        )


    def update(self):

        """Run one complete simulation iteration."""


        # 1. Life evolves.

        self.world = next_generation(self.world)


        # 2. Ribosome moves, senses and occasionally reads DNA.

        self.ribosome.step(self.world)


        # 3. Update the display.

        self.draw_world()

        self.update_dna_display()


        if self.ribosome.selected_gene is None:

            gene_text = "waiting"

        else:

            gene_text = str(self.ribosome.selected_gene)


        selected = (

            "none"

            if self.selected_dna_position is None

            else str(self.selected_dna_position)

        )


        reading = (

            f"{self.ribosome.reading_cursor}/"

            f"{len(self.ribosome.reading_positions)}"

            if self.ribosome.reading_positions

            else "0/0"

        )


        self.info.config(

            text=(

                f"Ribosome: ({self.ribosome.x}, {self.ribosome.y})   "

                f"Temperature: {self.ribosome.temperature}   "

                f"Gene: {gene_text} / {len(self.ribosome.genes)}   "

                f"Reading: {reading}   "

                f"Cooldown: {self.ribosome.gene_cooldown}   "

                f"Selected DNA position: {selected}"

            )

        )


        self.root.after(UPDATE_MS, self.update)



# ============================================================

# Main

# ============================================================


def main():

    """Initialize the world, DNA and ribosome, then start the GUI."""


    world = create_random_world()

    dna = create_dna(DNA_LENGTH)

    ribosome = Ribosome(dna)


    print("DNA:")

    print(dna)

    print()

    print(f"DNA length: {len(dna)}")

    print(f"Number of genes: {len(ribosome.genes)}")


    root = tk.Tk()

    LifeApp(root, world, ribosome)

    root.mainloop()



if __name__ == "__main__":

    main()


No comments:

Post a Comment