Page 2 of 2

Re: Miscellaneous discussion of CA algorithms and software

Posted: June 6th, 2025, 9:04 am
by b-engine
Decide to share a rule table making interface which could been ran in browser. Currently only fully support Moore neighbourhood rules. Not very advanced, but supports variable, transitions, colors and names, or even creating multiple distinct variables with same list of states.

Code: Select all

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>RulegolfTable</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 10px; }
        input, select, button { margin: 5px; font-size: 14px; }
        button { padding: 5px 8px; }
        .section { margin: 10px 0; border: 1px solid #ddd; padding: 8px; border-radius: 5px; }
        .icon-btn { font-size: 18px; min-width: 30px; }
        .var-list, .trans-list { margin: 5px 0; }
        .dialog { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 15px; border: 1px solid #000; z-index: 100; max-width: 90%; max-height: 90%; overflow: auto; }
        .dialog input, .dialog select { margin: 5px 0; width: 100%; }
        .state-option { display: inline-block; width: 24px; height: 24px; text-align: center; line-height: 24px; margin: 2px; border: 1px solid #000; }
        .trans-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; margin: 10px 0; }
        .trans-cell { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border: 1px solid #000; position: relative; }
        .trans-center { grid-column: 3; grid-row: 2; background-color: #f0f0f0; }
        .trans-arrow { grid-column: 5; grid-row: 2; display: flex; align-items: center; justify-content: center; font-size: 24px; }
        .trans-result { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border: 1px solid #000; }
        .var-options { display: flex; flex-wrap: wrap; margin: 5px 0; }
        .var-option { margin: 2px; padding: 2px 5px; background: #eee; border-radius: 3px; cursor: pointer; }
        .var-option:hover { background: #ddd; }
        .multi-var { margin-top: 10px; }
        .export-box { width: 100%; height: 300px; font-family: monospace; margin: 10px 0; }
        .grid-pos-0 { grid-column: 3; grid-row: 1; } /* Top */
        .grid-pos-1 { grid-column: 4; grid-row: 1; } /* Top-right */
        .grid-pos-2 { grid-column: 4; grid-row: 2; } /* Right */
        .grid-pos-3 { grid-column: 4; grid-row: 3; } /* Bottom-right */
        .grid-pos-4 { grid-column: 3; grid-row: 3; } /* Bottom */
        .grid-pos-5 { grid-column: 2; grid-row: 3; } /* Bottom-left */
        .grid-pos-6 { grid-column: 2; grid-row: 2; } /* Left */
        .grid-pos-7 { grid-column: 2; grid-row: 1; } /* Top-left */
        .grid-pos-8 { grid-column: 3; grid-row: 2; } /* Center */
        .grid-pos-9 { grid-column: 5; grid-row: 2; } /* Result */
    </style>
</head>
<body>
    <h1>RulegolfTable</h1>
    
    <div class="section">
        <div>📛 Rule name: <input type="text" id="ruleName" value="MyRule"></div>
        <div>🔢 States: <input type="number" id="numStates" min="2" value="2"></div>
        <div>🏘️ Neighborhood: 
            <select id="neighborhood">
                <option value="moore">Moore</option>
                <option value="vonNeumann">von Neumann</option>
                <option value="hexagonal">Hexagonal</option>
            </select>
        </div>
        <div>🔄 Symmetries: 
            <select id="symmetries">
                <option value="none">None</option>
                <option value="reflect_horizontal">Reflect Horizontal</option>
                <option value="reflect_vertical">Reflect Vertical</option>
                <option value="rotate4">Rotate 4</option>
                <option value="rotate8">Rotate 8</option>
                <option value="rotate4reflect">Rotate4 + Reflect</option>
                <option value="permute">Permute</option>
            </select>
        </div>
    </div>
    
    <div class="section">
        <button class="icon-btn" onclick="showVarDialog()">➕ Add Variable</button>
        <button class="icon-btn" onclick="showTransDialog()">🔀 Add Transition</button>
        <button class="icon-btn" onclick="showColorDialog()">🎨 Set Color</button>
        <button class="icon-btn" onclick="showNameDialog()">🏷️ Set State Name</button>
        <button class="icon-btn" onclick="showExportDialog()">📋 Export Rule</button>
    </div>
    
    <div class="section">
        <h3>📊 Variables</h3>
        <div id="varList" class="var-list"></div>
        
        <h3>🔄 Transitions</h3>
        <div id="transList" class="trans-list"></div>
    </div>
    
    <!-- Dialogs -->
    <div id="varDialog" class="dialog">
        <h3>➕ Add Variable</h3>
        Name: <input type="text" id="varName" placeholder="Base name for multiple variables"><br>
        <div class="var-options">
            <div class="var-option" onclick="setVarType('states')">States</div>
            <div class="var-option" onclick="setVarType('variable')">Same as Variable</div>
        </div>
        <div id="varStates"></div>
        <div id="varVariable" style="display:none">
            Variable: <select id="varRefVariable"></select>
        </div>
        <div class="multi-var">
            Create multiple: <input type="number" id="varCount" min="1" value="1" style="width:50px">
            <label><input type="checkbox" id="varAddSuffix"> Add numeric suffix</label>
        </div>
        <button onclick="addVariable()">Add</button>
        <button onclick="hideDialog('varDialog')">Cancel</button>
    </div>
    
    <div id="transDialog" class="dialog">
        <h3>🔀 Add Transition</h3>
        <div class="trans-grid" id="transGrid">
            <!-- Positions will be added dynamically in the correct order -->
            <div class="trans-cell grid-pos-0" title="Top"></div>
            <div class="trans-cell grid-pos-1" title="Top-right"></div>
            <div class="trans-cell grid-pos-2" title="Right"></div>
            <div class="trans-cell grid-pos-3" title="Bottom-right"></div>
            <div class="trans-cell grid-pos-4" title="Bottom"></div>
            <div class="trans-cell grid-pos-5" title="Bottom-left"></div>
            <div class="trans-cell grid-pos-6" title="Left"></div>
            <div class="trans-cell grid-pos-7" title="Top-left"></div>
            <div class="trans-cell grid-pos-8 trans-center" title="Center"></div>
            <div class="trans-arrow">→</div>
            <div class="trans-cell grid-pos-9" id="transResultPreview" title="Result"></div>
        </div>
        <button onclick="addTransition()">Add</button>
        <button onclick="hideDialog('transDialog')">Cancel</button>
    </div>
    
    <div id="editVarDialog" class="dialog">
        <h3>✏️ Edit Variable</h3>
        Name: <input type="text" id="editVarName"><br>
        <div class="var-options">
            <div class="var-option" onclick="setEditVarType('states')">States</div>
            <div class="var-option" onclick="setEditVarType('variable')">Same as Variable</div>
        </div>
        <div id="editVarStates"></div>
        <div id="editVarVariable" style="display:none">
            Variable: <select id="editVarRefVariable"></select>
        </div>
        <button onclick="saveVariable()">Save</button>
        <button onclick="hideDialog('editVarDialog')">Cancel</button>
    </div>
    
    <div id="editTransDialog" class="dialog">
        <h3>✏️ Edit Transition</h3>
        <div class="trans-grid" id="editTransGrid">
            <!-- Positions will be added dynamically in the correct order -->
            <div class="trans-cell grid-pos-0" title="Top"></div>
            <div class="trans-cell grid-pos-1" title="Top-right"></div>
            <div class="trans-cell grid-pos-2" title="Right"></div>
            <div class="trans-cell grid-pos-3" title="Bottom-right"></div>
            <div class="trans-cell grid-pos-4" title="Bottom"></div>
            <div class="trans-cell grid-pos-5" title="Bottom-left"></div>
            <div class="trans-cell grid-pos-6" title="Left"></div>
            <div class="trans-cell grid-pos-7" title="Top-left"></div>
            <div class="trans-cell grid-pos-8 trans-center" title="Center"></div>
            <div class="trans-arrow">→</div>
            <div class="trans-cell grid-pos-9" id="editTransResultPreview" title="Result"></div>
        </div>
        Sort order: <input type="number" id="editTransOrder" min="0" value="0"><br>
        <button onclick="saveTransition()">Save</button>
        <button onclick="hideDialog('editTransDialog')">Cancel</button>
    </div>
    
    <div id="colorDialog" class="dialog">
        <h3>🎨 Set Color</h3>
        State: <select id="colorState"></select><br>
        Color: <input type="color" id="stateColor" value="#000000"><br>
        <button onclick="saveColor()">Save</button>
        <button onclick="hideDialog('colorDialog')">Cancel</button>
    </div>
    
    <div id="nameDialog" class="dialog">
        <h3>🏷️ Set State Name</h3>
        State: <select id="nameState"></select><br>
        Name: <input type="text" id="stateName"><br>
        <button onclick="saveName()">Save</button>
        <button onclick="hideDialog('nameDialog')">Cancel</button>
    </div>

    <div id="exportDialog" class="dialog">
        <h3>📋 Export Rule</h3>
        <textarea id="exportBox" class="export-box" readonly></textarea>
        <button onclick="copyToClipboard()">Copy to Clipboard</button>
        <button onclick="hideDialog('exportDialog')">Close</button>
    </div>

    <script>
        // Data storage
        let variables = [];
        let transitions = [];
        let stateColors = {};
        let stateNames = {};
        let editingIndex = -1;
        let currentVarType = 'states';
        let currentEditVarType = 'states';
        
        // Neighborhood positions in the correct order:
        // Center (0), Top (1), Right (2), Bottom (3), Left (4), 
        // Top-right (5), Bottom-right (6), Bottom-left (7), Top-left (8)
        const neighborhoodOrder = [8, 0, 1, 2, 3, 4, 5, 6, 7]; // Center first, then orthogonals, then diagonals
        const displayOrder = [0,1,2,3,4,5,6,7,8]; // For visual display
        
        // Initialize
        document.addEventListener('DOMContentLoaded', function() {
            updateNumStates();
            document.getElementById('numStates').addEventListener('change', updateNumStates);
            
            // Setup the transition grids
            setupTransitionGrid('transGrid');
            setupTransitionGrid('editTransGrid');
        });
        
        function setupTransitionGrid(gridId) {
            const grid = document.getElementById(gridId);
            
            // Clear only the select elements, keep the grid structure
            const cells = grid.querySelectorAll('.trans-cell');
            cells.forEach(cell => {
                if (cell.id !== 'transResultPreview' && cell.id !== 'editTransResultPreview') {
                    cell.innerHTML = '';
                    const select = document.createElement('select');
                    select.id = `${gridId}N${cell.title.replace('-', '').toLowerCase()}`;
                    cell.appendChild(select);
                }
            });
        }
        
        function updateNumStates() {
            const num = parseInt(document.getElementById('numStates').value);
            // Initialize colors and names for states
            for (let i = 0; i < num; i++) {
                if (!stateColors[i]) stateColors[i] = getRandomColor();
                if (!stateNames[i]) stateNames[i] = `State ${i}`;
            }
            updateVarOptions();
        }
        
        function getRandomColor() {
            return '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
        }
        
        function updateVarOptions() {
            const varSelect = document.getElementById('varRefVariable');
            const editVarSelect = document.getElementById('editVarRefVariable');
            varSelect.innerHTML = '';
            editVarSelect.innerHTML = '';
            
            variables.forEach((v, i) => {
                varSelect.appendChild(new Option(v.name, i));
                editVarSelect.appendChild(new Option(v.name, i));
            });
        }
        
        function setVarType(type) {
            currentVarType = type;
            document.getElementById('varStates').style.display = type === 'states' ? 'block' : 'none';
            document.getElementById('varVariable').style.display = type === 'variable' ? 'block' : 'none';
        }
        
        function setEditVarType(type) {
            currentEditVarType = type;
            document.getElementById('editVarStates').style.display = type === 'states' ? 'block' : 'none';
            document.getElementById('editVarVariable').style.display = type === 'variable' ? 'block' : 'none';
        }
        
        // Dialog functions
        function showVarDialog() {
            const numStates = parseInt(document.getElementById('numStates').value);
            let html = '';
            for (let i = 0; i < numStates; i++) {
                html += `<input type="checkbox" id="varState${i}" value="${i}">
                         <label for="varState${i}"><span class="state-option" style="background:${stateColors[i]}">${i}</span> ${stateNames[i]}</label><br>`;
            }
            document.getElementById('varStates').innerHTML = html;
            document.getElementById('varName').value = '';
            document.getElementById('varCount').value = 1;
            document.getElementById('varAddSuffix').checked = false;
            setVarType('states');
            updateVarOptions();
            showDialog('varDialog');
        }
        
        function createStateOption(state, prefix, selected) {
            const option = document.createElement('option');
            option.value = state;
            option.textContent = state;
            if (selected) option.selected = true;
            
            const colorSpan = document.createElement('span');
            colorSpan.className = 'state-option';
            colorSpan.style.backgroundColor = stateColors[state];
            colorSpan.textContent = state;
            
            option.prepend(colorSpan.cloneNode(true));
            return option;
        }
        
        function populateTransitionGrid(gridId, numStates, pattern) {
            // Order for display: Center, Top, Right, Bottom, Left, Top-right, Bottom-right, Bottom-left, Top-left
            const displayPositions = ['center', 'top', 'right', 'bottom', 'left', 'topright', 'bottomright', 'bottomleft', 'topleft'];
            
            displayPositions.forEach((pos, index) => {
                const select = document.getElementById(`${gridId}N${pos}`);
                if (!select) return;
                
                select.innerHTML = '';
                
                const anyOption = document.createElement('option');
                select.appendChild(anyOption);
                
                for (let i = 0; i < numStates; i++) {
                    const selected = pattern && pattern[index] === i;
                    select.appendChild(createStateOption(i, gridId, selected));
                }
                
                // Add variables
                variables.forEach((v, vi) => {
                    const varOption = document.createElement('option');
                    varOption.value = `var${vi}`;
                    varOption.textContent = v.name;
                    if (pattern && pattern[index] === `var${vi}`) varOption.selected = true;
                    select.appendChild(varOption);
                });
            });
            
            // Handle result preview
            if (gridId === 'transGrid') {
                const resultPreview = document.getElementById('transResultPreview');
                resultPreview.innerHTML = '';
                const resultSelect = document.createElement('select');
                resultSelect.id = 'transResult';
                
                for (let i = 0; i < numStates; i++) {
                    resultSelect.appendChild(createStateOption(i, 'trans'));
                }
                
                resultSelect.addEventListener('change', function() {
                    updateResultPreview(resultPreview, this.value);
                });
                
                resultPreview.appendChild(resultSelect);
                updateResultPreview(resultPreview, '0');
            } else if (gridId === 'editTransGrid') {
                const resultPreview = document.getElementById('editTransResultPreview');
                resultPreview.innerHTML = '';
                const resultSelect = document.createElement('select');
                resultSelect.id = 'editTransResult';
                
                for (let i = 0; i < numStates; i++) {
                    const selected = pattern && pattern.result === i;
                    resultSelect.appendChild(createStateOption(i, 'editTrans', selected));
                }
                
                resultSelect.addEventListener('change', function() {
                    updateResultPreview(resultPreview, this.value);
                });
                
                resultPreview.appendChild(resultSelect);
                if (pattern) {
                    updateResultPreview(resultPreview, pattern.result);
                } else {
                    updateResultPreview(resultPreview, '0');
                }
            }
        }
        
        function showTransDialog() {
            const numStates = parseInt(document.getElementById('numStates').value);
            
            // Populate the grid
            populateTransitionGrid('transGrid', numStates);
            
            showDialog('transDialog');
        }
        
        function updateResultPreview(previewEl, state) {
            previewEl.style.backgroundColor = stateColors[state] || '#ffffff';
            if (previewEl.querySelector('select')) {
                previewEl.querySelector('select').value = state;
            }
        }
        
        function showEditVarDialog(index) {
            editingIndex = index;
            const varData = variables[index];
            document.getElementById('editVarName').value = varData.name;
            
            if (varData.reference !== undefined) {
                setEditVarType('variable');
                document.getElementById('editVarRefVariable').value = varData.reference;
            } else {
                setEditVarType('states');
                const numStates = parseInt(document.getElementById('numStates').value);
                let html = '';
                for (let i = 0; i < numStates; i++) {
                    const checked = varData.states.includes(i) ? 'checked' : '';
                    html += `<input type="checkbox" id="editVarState${i}" value="${i}" ${checked}>
                             <label for="editVarState${i}"><span class="state-option" style="background:${stateColors[i]}">${i}</span> ${stateNames[i]}</label><br>`;
                }
                document.getElementById('editVarStates').innerHTML = html;
            }
            
            updateVarOptions();
            showDialog('editVarDialog');
        }
        
        function showEditTransDialog(index) {
            editingIndex = index;
            const trans = transitions[index];
            const numStates = parseInt(document.getElementById('numStates').value);
            
            // Convert the pattern to display order
            const displayPattern = [
                trans.pattern[0], // Center
                trans.pattern[1], // Top
                trans.pattern[2], // Right
                trans.pattern[3], // Bottom
                trans.pattern[4], // Left
                trans.pattern[5], // Top-right
                trans.pattern[6], // Bottom-right
                trans.pattern[7], // Bottom-left
                trans.pattern[8]  // Top-left
            ];
            
            // Populate the grid with existing pattern
            populateTransitionGrid('editTransGrid', numStates, {
                ...trans,
                pattern: displayPattern
            });
            
            document.getElementById('editTransOrder').value = trans.order || 0;
            showDialog('editTransDialog');
        }
        
        function showColorDialog() {
            const numStates = parseInt(document.getElementById('numStates').value);
            let html = '';
            for (let i = 0; i < numStates; i++) {
                html += `<option value="${i}">${i}: ${stateNames[i]}</option>`;
            }
            document.getElementById('colorState').innerHTML = html;
            showDialog('colorDialog');
        }
        
        function showNameDialog() {
            const numStates = parseInt(document.getElementById('numStates').value);
            let html = '';
            for (let i = 0; i < numStates; i++) {
                html += `<option value="${i}">${i}: ${stateNames[i]}</option>`;
            }
            document.getElementById('nameState').innerHTML = html;
            showDialog('nameDialog');
        }
        
        function showExportDialog() {
            const exportText = generateRuleText();
            document.getElementById('exportBox').value = exportText;
            showDialog('exportDialog');
        }
        
        function showDialog(id) {
            document.getElementById(id).style.display = 'block';
        }
        
        function hideDialog(id) {
            document.getElementById(id).style.display = 'none';
        }
        
        // Data manipulation functions
        function addVariable() {
            const baseName = document.getElementById('varName').value.trim();
            if (!baseName) return;
            
            const count = parseInt(document.getElementById('varCount').value);
            const addSuffix = document.getElementById('varAddSuffix').checked;
            
            for (let c = 0; c < count; c++) {
                const name = addSuffix ? `${baseName}${c || ''}` : baseName;
                
                let varData;
                if (currentVarType === 'variable') {
                    const refIndex = parseInt(document.getElementById('varRefVariable').value);
                    if (isNaN(refIndex)) return;
                    varData = { name, reference: refIndex };
                } else {
                    const numStates = parseInt(document.getElementById('numStates').value);
                    const states = [];
                    for (let i = 0; i < numStates; i++) {
                        if (document.getElementById(`varState${i}`).checked) {
                            states.push(i);
                        }
                    }
                    varData = { name, states };
                }
                
                variables.push(varData);
            }
            
            updateVarList();
            updateVarOptions();
            hideDialog('varDialog');
        }
        
        function saveVariable() {
            if (editingIndex === -1) return;
            
            const name = document.getElementById('editVarName').value.trim();
            if (!name) return;
            
            let varData;
            if (currentEditVarType === 'variable') {
                const refIndex = parseInt(document.getElementById('editVarRefVariable').value);
                if (isNaN(refIndex)) return;
                varData = { name, reference: refIndex };
            } else {
                const numStates = parseInt(document.getElementById('numStates').value);
                const states = [];
                for (let i = 0; i < numStates; i++) {
                    if (document.getElementById(`editVarState${i}`).checked) {
                        states.push(i);
                    }
                }
                varData = { name, states };
            }
            
            variables[editingIndex] = varData;
            updateVarList();
            updateVarOptions();
            hideDialog('editVarDialog');
            editingIndex = -1;
        }
        
        function addTransition() {
            // Get values in display order: Center, Top, Right, Bottom, Left, Top-right, Bottom-right, Bottom-left, Top-left
            const displayPositions = ['center', 'top', 'right', 'bottom', 'left', 'topright', 'bottomright', 'bottomleft', 'topleft'];
            
            const pattern = displayPositions.map(pos => {
                const val = document.getElementById(`transGridN${pos}`).value;
                if (val.startsWith('var')) {
                    return val;
                } else {
                    return val === '' ? null : parseInt(val);
                }
            });
            
            const result = parseInt(document.getElementById('transResult').value);
            
            transitions.push({ 
                pattern: [
                    pattern[0], // Center
                    pattern[1], // Top
                    pattern[2], // Right
                    pattern[3], // Bottom
                    pattern[4], // Left
                    pattern[5], // Top-right
                    pattern[6], // Bottom-right
                    pattern[7], // Bottom-left
                    pattern[8]  // Top-left
                ], 
                result, 
                order: 0 
            });
            
            updateTransList();
            hideDialog('transDialog');
        }
        
        function saveTransition() {
            if (editingIndex === -1) return;
            
            // Get values in display order: Center, Top, Right, Bottom, Left, Top-right, Bottom-right, Bottom-left, Top-left
            const displayPositions = ['center', 'top', 'right', 'bottom', 'left', 'topright', 'bottomright', 'bottomleft', 'topleft'];
            
            const pattern = displayPositions.map(pos => {
                const val = document.getElementById(`editTransGridN${pos}`).value;
                if (val.startsWith('var')) {
                    return val;
                } else {
                    return val === '' ? null : parseInt(val);
                }
            });
            
            const result = parseInt(document.getElementById('editTransResult').value);
            const order = parseInt(document.getElementById('editTransOrder').value);
            
            transitions[editingIndex] = { 
                pattern: [
                    pattern[0], // Center
                    pattern[1], // Top
                    pattern[2], // Right
                    pattern[3], // Bottom
                    pattern[4], // Left
                    pattern[5], // Top-right
                    pattern[6], // Bottom-right
                    pattern[7], // Bottom-left
                    pattern[8]  // Top-left
                ],
                result, 
                order 
            };
            
            updateTransList();
            hideDialog('editTransDialog');
            editingIndex = -1;
        }
        
        function saveColor() {
            const state = parseInt(document.getElementById('colorState').value);
            const color = document.getElementById('stateColor').value;
            stateColors[state] = color;
            hideDialog('colorDialog');
        }
        
        function saveName() {
            const state = parseInt(document.getElementById('nameState').value);
            const name = document.getElementById('stateName').value.trim();
            stateNames[state] = name || `State ${state}`;
            hideDialog('nameDialog');
            updateVarList();
            updateTransList();
        }
        
        function deleteVariable(index) {
            variables.splice(index, 1);
            updateVarList();
            updateVarOptions();
        }
        
        function deleteTransition(index) {
            transitions.splice(index, 1);
            updateTransList();
        }
        
        // UI update functions
        function updateVarList() {
            const container = document.getElementById('varList');
            container.innerHTML = '';
            
            variables.forEach((varData, index) => {
                const btn = document.createElement('button');
                btn.className = 'icon-btn';
                
                let displayText;
                if (varData.reference !== undefined) {
                    const refVar = variables[varData.reference];
                    displayText = `${varData.name} = ${refVar.name}`;
                } else {
                    displayText = `${varData.name} = {${varData.states.join(',')}}`;
                }
                
                btn.innerHTML = '✏️ ' + displayText;
                btn.onclick = () => showEditVarDialog(index);
                
                const delBtn = document.createElement('button');
                delBtn.className = 'icon-btn';
                delBtn.innerHTML = '❌';
                delBtn.onclick = (e) => { e.stopPropagation(); deleteVariable(index); };
                
                const div = document.createElement('div');
                div.appendChild(btn);
                div.appendChild(delBtn);
                container.appendChild(div);
            });
        }
        
        function updateTransList() {
            const container = document.getElementById('transList');
            container.innerHTML = '';
            
            // Sort transitions by order
            transitions.sort((a, b) => (a.order || 0) - (b.order || 0));
            
            transitions.forEach((trans, index) => {
                const patternStr = [
                    trans.pattern[0], // Center
                    trans.pattern[1], // Top
                    trans.pattern[2], // Right
                    trans.pattern[3], // Bottom
                    trans.pattern[4], // Left
                    trans.pattern[5], // Top-right
                    trans.pattern[6], // Bottom-right
                    trans.pattern[7], // Bottom-left
                    trans.pattern[8]  // Top-left
                ].map(s => {
                    if (s === null) return '*';
                    if (typeof s === 'string' && s.startsWith('var')) {
                        const varIndex = parseInt(s.substring(3));
                        return variables[varIndex].name;
                    }
                    return s;
                }).join(',');
                
                const resultStr = stateNames[trans.result] || 'State '+trans.result;
                
                const btn = document.createElement('button');
                btn.className = 'icon-btn';
                btn.innerHTML = '✏️ ' + patternStr + ' → ' + resultStr;
                btn.onclick = () => showEditTransDialog(index);
                
                const delBtn = document.createElement('button');
                delBtn.className = 'icon-btn';
                delBtn.innerHTML = '❌';
                delBtn.onclick = (e) => { e.stopPropagation(); deleteTransition(index); };
                
                const div = document.createElement('div');
                div.appendChild(btn);
                div.appendChild(delBtn);
                container.appendChild(div);
            });
        }
        
        // Export function
        function generateRuleText() {
            const ruleName = document.getElementById('ruleName').value;
            const numStates = document.getElementById('numStates').value;
            const neighborhood = document.getElementById('neighborhood').value;
            const symmetries = document.getElementById('symmetries').value;
            
            let ruleText = `@RULE ${ruleName}\n\n`;
            ruleText += `@TABLE\n`;
            ruleText += `n_states:${numStates}\n`;
            ruleText += `neighborhood:${neighborhood}\n`;
            ruleText += `symmetries:${symmetries}\n\n`;
            
            // Add variables
            variables.forEach(varData => {
                if (varData.reference !== undefined) {
                    const refVar = variables[varData.reference];
                    ruleText += `var ${varData.name} = ${refVar.name}\n`;
                } else {
                    ruleText += `var ${varData.name} = {${varData.states.join(',')}}\n`;
                }
            });
            
            if (variables.length > 0) ruleText += '\n';
            
            // Add transitions
            transitions.forEach(trans => {
                // Format pattern in the correct order: Center, Top, Right, Bottom, Left, Top-right, Bottom-right, Bottom-left, Top-left
                const pattern = [
                    trans.pattern[0], // Center
                    trans.pattern[1], // Top
                    trans.pattern[5], // Right
                    trans.pattern[2], // Bottom
                    trans.pattern[6], // Left
                    trans.pattern[3], // Top-right
                    trans.pattern[7], // Bottom-right
                    trans.pattern[4], // Bottom-left
                    trans.pattern[8]  // Top-left
                ];
                
                const patternStr = pattern.map(s => {
                    if (s === null) return '*';
                    if (typeof s === 'string' && s.startsWith('var')) {
                        const varIndex = parseInt(s.substring(3));
                        return variables[varIndex].name;
                    }
                    return s;
                }).join(',');
                
                ruleText += patternStr + ',';
                ruleText += trans.result + '\n\n';
            });
            
            // Add colors
            ruleText += `@COLORS\n`;
            for (const [state, color] of Object.entries(stateColors)) {
                const rgb = hexToRgb(color);
                ruleText += `${state} ${rgb.r} ${rgb.g} ${rgb.b}\n`;
            }
            
            return ruleText;
        }
        
        function copyToClipboard() {
            const exportText = document.getElementById('exportBox').value;
            navigator.clipboard.writeText(exportText).then(() => {
                alert('Rule copied to clipboard!');
            }).catch(err => {
                console.error('Failed to copy: ', err);
            });
        }
        
        function hexToRgb(hex) {
            const r = parseInt(hex.slice(1, 3), 16);
            const g = parseInt(hex.slice(3, 5), 16);
            const b = parseInt(hex.slice(5, 7), 16);
            return { r, g, b };
        }
    </script>
</body>
</html>
(Is there any more appriotiate thread to share such scripts?)

EDIT: this is a HTML + JS script. To run it, paste it into a text editor and save it as HTML file.
EDIT 2: It's indeed "revolutionary" (significantly lowering the barrier of ruletable golfing), but I'm not sure if this is worthy of a separate thread. Tell me if this is notable enough.

EDIT 3: GitHub repository at here.

Re: Miscellaneous discussion of CA algorithms and software

Posted: January 8th, 2026, 5:09 pm
by LaundryPizza03
Updated version of getallisorule2.py, fixes several bugs and now returns rules in standard notation. A bugfixed version of rulestringopt is still present for programs that need it.

Code: Select all

#getallisorule3.py, used for the next script.
# Rule computation script for use with Golly.
# Author: Nathaniel Johnston ([email protected]), June 2009.
# Updated by: Peter, NASZVADI (), June 2017.
# Slightly modified by AforAmpere
# Updated and bugfixed by LaundryPizza03

# Gives the maximal family of rules that a still life, oscillator, or spaceship
# works under. Must be called while the rule is set of one such family
# For example, to find out what rules a glider works in, first set the rule
# to Life or HighLife, not Seeds.
# Handles nontotalistic rules, too, so it needs Golly 2.8 or newer.

import golly as g
from glife import validint

Hensel = [
    ['0'],
    ['1c', '1e'],
    ['2a', '2c', '2e', '2i', '2k', '2n'],
    ['3a', '3c', '3e', '3i', '3j', '3k', '3n', '3q', '3r', '3y'],
    ['4a', '4c', '4e', '4i', '4j', '4k', '4n', '4q', '4r', '4t', '4w', '4y', '4z'],
    ['5a', '5c', '5e', '5i', '5j', '5k', '5n', '5q', '5r', '5y'],
    ['6a', '6c', '6e', '6i', '6k', '6n'],
    ['7c', '7e'],
    ['8']
]

# Python versions < 2.4 don't have "sorted" built-in
try:
    sorted
except NameError:
    def sorted(inlist):
        outlist = list(inlist)
        outlist.sort()
        return outlist

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

def chunks(l, n):
    for i in range(0, len(l), n):
        yield l[i:i+n]

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

def rulestringopt(a):
    result = ''
    context = ''
    lastnum = ''
    lastcontext = ''
    for i in a:
        if i in 'BS':
            context = i
            result += i
        elif i in '012345678':
            if (i == lastnum) and (lastcontext == context):
                pass
            else:
                lastcontext = context
                lastnum = i
                result += i
        elif i in 'aceijknqrtwyz':
            result += i
        else:
            lastnum = ''
            lastcontext = ''
            result += i
        lastcontext = context
    result = str.replace(result, '4aceijknqrtwyz', '4')
    result = str.replace(result, '3aceijknqry', '3')
    result = str.replace(result, '5aceijknqry', '5')
    result = str.replace(result, '2aceikn', '2')
    result = str.replace(result, '6aceikn', '6')
    result = str.replace(result, '1ce', '1')
    result = str.replace(result, '7ce', '7')
    return result

clist = []
rule = g.getrule().split(':')[0]

fuzzer = rule + '9'
oldrule = rule
rule = ''
context = ''
deletefrom = []
for i in fuzzer:
    if i == '-':
        deletefrom = [x[1] for x in Hensel[int(context)]]
    elif i in '0123456789/S':
        if deletefrom:
            rule += ''.join(deletefrom)
            deletefrom = []
        context = i
    if len(deletefrom) == 0:
        rule += i
    elif i in deletefrom:
        deletefrom.remove(i)
rule = rule.strip('9')

if not (rule[0] == 'B' and '/S' in rule):
    g.exit('Please set Golly to a Life-like rule.')

if g.empty():
    g.exit('The pattern is empty.')

s = g.getstring('Enter the period:', '', 'Rules calculator')

if not validint(s):
    g.exit('Bad number: %s' % s)

numsteps = int(s)
if numsteps < 1:
    g.exit('Period must be at least 1.')

g.select(g.getrect())
g.copy()
s = int(s)

g.new('')
g.paste(0, 0, 'or')
for i in range(0,s):
    g.run(1)
    clist.append(list(chunks(g.getcells(g.getrect()), 2)))
    mcc = 0 if len(clist[i]) == 0 else min(clist[i])
    clist[i] = [[x[0] - mcc[0], x[1] - mcc[1]] for x in clist[i]]

g.show('Processing...')

ruleArr = rule.split('/')
ruleArr[0] = ruleArr[0].lstrip('B')
ruleArr[1] = ruleArr[1].lstrip('S')

b_need = []
b_OK = []
s_need = []
s_OK = []

context = ''
fuzzed = ruleArr[0] + '9'
for i in fuzzed:
    if i in '0123456789':
        if len(context) == 1:
            b_need += Hensel[int(context)]
            b_OK += Hensel[int(context)]
        context = i
    elif context != '':
        b_need.append(context[0] + i)
        b_OK.append(context[0] + i)
        context += context[0]
context = ''

fuzzed = ruleArr[1] + '9'
for i in fuzzed:
    if i in '0123456789':
        if len(context) == 1:
            s_need += Hensel[int(context)]
            s_OK += Hensel[int(context)]
        context = i
    elif context != '':
        s_need.append(context[0] + i)
        s_OK.append(context[0] + i)
        context += context[0]

for i in [iter2 for iter1 in Hensel for iter2 in iter1]:
    if not i in b_OK and i != '0': # B0 and nontotalistic rulestrings are mutually exclusive
        g.new('')
        g.paste(0, 0, 'or')
        g.select(g.getrect())
        b_OK.append(i)
        execfor = 1
        try:
            g.setrule('B' + ''.join(b_OK) + '/S' + ruleArr[1])
        except:
            b_OK.remove(i)
            execfor = 0
        for j in range(0, s * execfor):
            g.run(1)
            try:
                dlist = list(chunks(g.getcells(g.getrect()), 2))
                mcc = 0 if len(dlist) == 0 else min(dlist)
                dlist = [[x[0] - mcc[0], x[1] - mcc[1]] for x in dlist]
                if not(clist[j] == dlist):
                    b_OK.remove(i)
                    break
            except:
                b_OK.remove(i)
                break
        b_OK.sort()

    if not i in s_OK:
        g.new('')
        g.paste(0, 0, 'or')
        g.select(g.getrect())
        s_OK.append(i)
        execfor = 1
        try:
            g.setrule('B' + ruleArr[0] + '/S' + ''.join(s_OK))
        except:
            s_OK.remove(i)
            execfor = 0
        for j in range(0, s * execfor):
            g.run(1)
            try:
                dlist = list(chunks(g.getcells(g.getrect()), 2))
                mcc = 0 if len(dlist) == 0 else min(dlist)
                dlist = [[x[0] - mcc[0], x[1] - mcc[1]] for x in dlist]
                if not(clist[j] == dlist):
                    s_OK.remove(i)
                    break
            except:
                s_OK.remove(i)
                break
        s_OK.sort()

    if i in b_need and i != '0': # B0 and nontotalistic rulestrings are mutually exclusive
        g.new('')
        g.paste(0, 0, 'or')
        g.select(g.getrect())
        b_need.remove(i)
        g.setrule('B' + ''.join(b_need) + '/S' + ruleArr[1])
        for j in range(0, s):
            g.run(1)
            try:
                dlist = list(chunks(g.getcells(g.getrect()), 2))
                mcc = 0 if len(dlist) == 0 else min(dlist)
                dlist = [[x[0] - mcc[0], x[1] - mcc[1]] for x in dlist]
                if not(clist[j] == dlist):
                    b_need.append(i)
                    break
            except:
                b_need.append(i)
                break
        b_need.sort()

    if i in s_need:
        g.new('')
        g.paste(0, 0, 'or')
        g.select(g.getrect())
        s_need.remove(i)
        g.setrule('B' + ruleArr[0] + '/S' + ''.join(s_need))
        for j in range(0, s):
            g.run(1)
            try:
                dlist = list(chunks(g.getcells(g.getrect()), 2))
                mcc = 0 if len(dlist) == 0 else min(dlist)
                dlist = [[x[0] - mcc[0], x[1] - mcc[1]] for x in dlist]
                if not(clist[j] == dlist):
                    s_need.append(i)
                    break
            except:
                s_need.append(i)
                break
        s_need.sort()

g.reset()
g.setrule('B' + ''.join(sorted(b_need)) + '/S' + ''.join(sorted(s_need)))
minrule = g.getrule()
g.setrule('B' + ''.join(sorted(b_OK)) + '/S' + ''.join(sorted(s_OK)))
maxrule = g.getrule()
ruleres = minrule + ' - ' + maxrule
g.show(ruleres)
g.setrule(oldrule)
g.getstring('Pattern works in 2^%d rules:'%(len(b_OK)-len(b_need)+len(s_OK)-len(s_need)), ruleres, 'Rules calculator')

Re: Miscellaneous discussion of CA algorithms and software

Posted: March 8th, 2026, 12:12 am
by R2INT
I have recently been experimenting with creating software that's designed to make pattern editing more efficient. The software that I present below is made entirely using AI tools. I found that building a p60 LWSS gun made from Gosper Glider Guns is approximately 1.6x faster in my software compared to constructing the same pattern in LifeViewer.

There were a few innovations that allow me to edit patterns more quickly. The biggest one is separating annotations from the pattern itself. LifeViewer does this by using OCA:LifeHistory, which I think you are familliar with. LifeHistory uses its annotations in two different states alongside the alive and dead cells. In my software, annotations are instead placed on a separate layer. They are not part of the simulation rule and therefore do not change when the pattern evolves.

Because this software is not sufficiently developed yet to enable external clipboard pasting, I needed to construct the Gosper glider guns in the pattern from memory. To do that, I have memorized the positioning of the queen bees just before they collide and form a glider, and the way I construct the gun is by advancing the 'gun' far enough to produce the beehives, and then place the blocks such that they eat the beehives. In LifeViewer, I must place the blocks for each beehive individually. However, in my software, annotations persist when the pattern is reset. This means I can mark both block positions using annotations and then reset the pattern only once before placing them.

I have measured the time it took to construct the LWSS guns starting after placing the syntheses. Here are the two glider placements I used:

Code: Select all

x = 20, y = 8, rule = LifeHistory
14.D.D$2.D.D10.2D$3.2D10.D$3.D2$2D11.2D3.D$.2D9.D.D2.2D$D13.D2.D.D!
In order to line up the guns to produce two gun (a p60 glider gun that uses a period doubling reaction), we need to line up the gliders. For LifeViewer, we need to select state 4 (requiring me to move my mouse to the top bar to select the state, and then move the mouse back), advance the pattern to generation 48 (for example), and then mark the glider from memory:

Code: Select all

x = 36, y = 17, rule = LifeHistory
24.A$22.A.A$12.2A6.2A12.2A$11.A3.A4.2A12.2A$2A8.A5.A3.2A$2A8.A3.A.2A4.
A.A$10.A5.A7.A$11.A3.A$12.2A6$27.D.D$28.2D$28.D!
In my software, however, you can just advance the gun 48 generations, select the glider, and then press Ctrl+M. That's very fast compared to switching to markoff and then drawing a glider.

Now let's talk about how my software handles annotations. LifeViewer's LifeHistory uses alive, dead, and two more states to mark cells. My software instead keeps annotations on a separate overlay layer that does not change when the pattern is simulated. Below is a screenshot showing annotated cells (this is still a preliminary version; the annotation display is not finalized yet):
Screenshot 2026-03-07 214930.png
Screenshot 2026-03-07 214930.png (14.71 KiB) Viewed 805 times
Here is a screenshot of the p60 LWSS gun that I built (note that my editor doesn't support RLE yet so I can't post it in RLE format):
Screenshot 2026-03-07 215840.png
Screenshot 2026-03-07 215840.png (12.69 KiB) Viewed 805 times
This software is currently unnamed, and I have not yet published a GitHub repository for it. This project is intended to streamline editing patterns. If you have any naming suggestions, please let me know!

EDIT: testing RLE export

Code: Select all

x = 21, y = 6, rule = B3/S23
[pat]9b3o9b$9bobo9b$2b2o5bobo5b2o2b$bobo13bobob$bo17bob$2o17b2o!
[ann]9.3A9.$9.A.A9.$2.2B5.A.A5.2B2.$.B.B13.B.B.$.B17.B.$2B17.2B!
EDIT2: eater collection and two annotated conduits:

Code: Select all

x = 232, y = 34, rule = B3/S23
[pat]232b$232b$232b$232b$93b2o137b$93b2o5b2o101bo28b$82b2o16b2o36bo62bobo11bo16b$83bo54b3o61b2o9b3o16b$83bobo55bo70bo19b$84b2o12b2o40b2o11b2o57b2o18b$98b2o53b2o42b2o33b$8bo95b2o92bo33b$6b3o23b2ob2o3b2o62b2o92bob2o30b$5bo24bo2bobobo2bobo156bo2bo29b$5b2o23b2obo2bo3bo159b2o30b$33bo181b2o15b$2b3o28b2o49bo54bo75b2o15b$4bo26b2o2bobo46bobo52bobo90b$3bo26bo2bo2b2o46b3o52b3o90b$31b2o53bo54bo90b$224bo3b2o2b$223bobo3bo2b$222bobo3bo3b$86b2ob2o127b2obobo3bo4b$84bo2bobobo126b2obo2b4obo2b$84b2obo2bo63b2o66bobo3bobob$87bo66bobo61b2ob2o2bo2bobob$87b2o67bo62bobo2b2o3bo2b$85b2o2bobo64b2o49b2o10bobo10b$84bo2bo2b2o115b2o11bo11b$85b2o145b$232b$232b$232b!
[ann]232.$232.$232.$232.$93.2C137.$93.2C5.2C101.B28.$82.2A16.2C36.C62.B.B11.C16.$83.A54.3C61.2B9.3C16.$83.A.A55.C70.C19.$84.2A12.2C40.2C11.2C11.D45.2C18.$98.2C53.2C9.3D30.2C33.$8.B95.2C58.D.D31.C33.$6.3B23.2A.2A3.2B62.2C58.D33.C.2C30.$5.B24.A2.A.A.A2.B.B156.C2.C23.D5.$5.2B23.2A.A2.A3.B159.2C24.D.D3.$33.A181.2C9.3D3.$2.3A28.2A49.B54.B75.2C11.D3.$4.A26.2A2.A.A46.B.B52.B.B90.$3.A26.A2.A2.2A46.3B52.3B90.$31.2A53.B54.B90.$224.C3.2C2.$223.C.C3.C2.$222.C.C3.C3.$86.2A.2A127.2C.C.C3.C4.$84.A2.A.A.A126.2C.C2.4C.C2.$84.2A.A2.A63.2C66.C.C3.C.C.$87.A8.3D55.C.C61.2C.2C2.C2.C.C.$87.2A7.D59.C62.C.C2.2C3.C2.$85.2A2.A.A3.3D58.2C49.2C10.C.C10.$84.A2.A2.2A115.2C11.C11.$85.2A145.$232.$232.$232.!