about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xawk/forth/f.awk369
-rwxr-xr-xawk/forth/old/f.awk344
-rw-r--r--awk/forth/old/test.forth44
-rw-r--r--awk/forth/test.forth34
-rwxr-xr-xawk/retro/retro.awk250
-rwxr-xr-xawk/retro/test.awk52
-rwxr-xr-xawk/retro/vm.awk364
-rwxr-xr-xawk/scheme/s.awk139
-rwxr-xr-xawk/vm/vm.awk208
-rw-r--r--awk/vm/vm_tests.sh30
-rw-r--r--chibi/pi.scm17
-rw-r--r--html/cards/cards.js441
-rw-r--r--html/cards/fonts/DotGothic16-Regular.ttfbin0 -> 2027048 bytes
-rw-r--r--html/cards/fonts/OFL copy.txt93
-rw-r--r--html/cards/fonts/OFL.txt93
-rw-r--r--html/cards/fonts/PressStart2P-Regular.ttfbin0 -> 116008 bytes
-rw-r--r--html/cards/index.html36
m---------html/cards/pokemon-font0
-rw-r--r--html/isometric-bounce/index.html (renamed from html/voxels/index.html)0
-rw-r--r--html/isometric-bounce/js/game.js (renamed from html/voxels/js/game.js)100
-rw-r--r--html/matt-chat/ChicagoFLF.ttfbin0 -> 31256 bytes
-rw-r--r--html/matt-chat/cat.pngbin0 -> 2573 bytes
-rw-r--r--html/matt-chat/com.user.server.plist16
-rw-r--r--html/matt-chat/index.html1266
-rw-r--r--html/matt-chat/pokemon.js157
-rwxr-xr-xhtml/matt-chat/server.sh23
-rw-r--r--html/read-write/index.html198
27 files changed, 4240 insertions, 34 deletions
diff --git a/awk/forth/f.awk b/awk/forth/f.awk
new file mode 100755
index 0000000..16de171
--- /dev/null
+++ b/awk/forth/f.awk
@@ -0,0 +1,369 @@
+#!/usr/bin/awk -f
+
+# I wanted to implement something non-trivial using awk. 
+# If I was clever I wouldn’t implement forth directly in awk,
+# instead I’d implement a simple virtual machine using awk, 
+# and then implement the forth using the virtual machine’s byte code
+# ...but there is only so much brain power I can exert on such a silly project.
+
+BEGIN {
+    # Initialize stacks and dictionaries
+    stack_ptr = 0
+    dict_size = 0
+    
+    # Built-in words, and some documentation (I could use stack comments,
+    # but I find those sort of unintuitive)
+    dict["+"] = "+     : Adds the top two numbers on the stack."
+    dict["-"] = "-     : Subtracts the top number from the second top number on the stack."
+    dict["*"] = "*     : Multiplies the top two numbers on the stack."
+    dict["/"] = "/     : Divides the second top number by the top number on the stack."
+    dict["."] = ".     : Prints the top of the stack."
+    dict[".s"] = ".s    : Shows all values on the stack."
+    dict["dup"] = "dup   : Duplicates the top value on the stack."
+    dict["drop"] = "drop  : Removes the top value from the stack."
+    dict["swap"] = "swap  : Swaps the top two values on the stack."
+    dict["over"] = "over  : Copies the second top value to the top of the stack."
+    dict["rot"] = "rot   : Rotates the top three values on the stack."
+    dict["="] = "=     : Compares the top two values for equality."
+    dict["<"] = "<     : Checks if the second top value is less than the top value."
+    dict[">"] = ">     : Checks if the second top value is greater than the top value."
+    dict["bye"] = "bye   : Exits the interpreter."
+    dict["words"] = "words : Lists all available words and their documentation."
+    
+    # State flags
+    compiling = 0
+    current_def = ""
+    def_name = ""
+    
+    # If an input file isn't specified, enter REPL mode
+    if (ARGC == 1) {
+        repl()
+    }
+}
+
+# Handle file input
+{
+    if (FILENAME ~ /\.forth$/) {
+        interpret($0)
+    }
+}
+
+function repl() {
+    print "f.awk! A forth interpreter.\nUse 'bye' to exit.\nUse 'words' to list all available words.\n"
+    while (1) {
+        printf "f> "
+        if (getline input < "/dev/tty" <= 0) break
+        interpret(input)
+    }
+}
+
+function interpret(line) {
+    gsub(/\(.*\)/, "", line)  # Remove everything from ( to )
+    gsub(/\\.*$/, "", line)   # Remove backslash comments, too
+
+    n = split(line, words, /[ \t]+/)
+    
+    for (i = 1; i <= n; i++) {
+        word = words[i]
+        if (word == "") continue
+        
+        # print "Processing word: " word
+        
+        if (word == ":") {
+            compiling = 1
+            i++
+            def_name = words[i]
+            current_def = ""
+            continue
+        }
+        
+        if (compiling) {
+            if (word == ";") {
+                # Store user-defined word with its name and definition
+                dict[def_name] = "word " current_def
+                compiling = 0
+                continue
+            }
+            current_def = current_def " " word
+            continue
+        }
+        
+        # Execute the word and skip further processing if it's .s
+        if (word == ".s") {
+            execute_word(word)
+            break  # Exit the loop after executing .s
+        }
+        
+        execute_word(word)
+    }
+}
+
+function execute_word(word) {
+    if (word ~ /^-?[0-9]+$/) {
+        push(word + 0)
+    } else if (word in dict) {
+        if (dict[word] ~ /^word /) {
+            # User-defined word
+            sequence = substr(dict[word], 6)
+            split(sequence, subwords, " ")
+            for (sw in subwords) {
+                if (subwords[sw] != "") {
+                    execute_word(subwords[sw])
+                }
+            }
+        } else {
+            # Built-in words
+            if (word == "+") math_add()
+            else if (word == "-") math_sub()
+            else if (word == "*") math_mul()
+            else if (word == "/") math_div()
+            else if (word == ".") stack_print()
+            else if (word == ".s") {
+                # print "Executing .s command"
+                stack_show()
+            }
+            else if (word == "dup") stack_dup()
+            else if (word == "drop") stack_drop()
+            else if (word == "swap") stack_swap()
+            else if (word == "over") stack_over()
+            else if (word == "rot") stack_rot()
+            else if (word == "=") compare_eq()
+            else if (word == "<") compare_lt()
+            else if (word == ">") compare_gt()
+            else if (word == "bye") exit_program()
+            else if (word == "words") list_words()
+            else if (word == "if") {
+                # Handle the if statement
+                if_condition = pop()
+                if (if_condition == 0) {
+                    # Skip to the next part until we find 'then' or 'else'
+                    skip_if = 1
+                }
+            }
+            else if (word == "else") {
+                # Handle the else statement
+                if (skip_if) {
+                    skip_if = 0  # Reset the skip flag
+                } else {
+                    # Skip to the next part until we find 'then'
+                    skip_else = 1
+                }
+            }
+            else if (word == "then") {
+                # End of the conditional
+                skip_if = 0
+                skip_else = 0
+            }
+        }
+    } else {
+        print "Error: Unknown word '" word "'"
+    }
+}
+
+function push(val) {
+    stack[stack_ptr++] = val
+}
+
+function pop() {
+    if (stack_ptr <= 0) {
+        print "Error: Stack underflow"
+        return 0
+    }
+    return stack[--stack_ptr]
+}
+
+function math_add() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a + b)
+}
+
+function math_sub() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a - b)
+}
+
+function math_mul() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a * b)
+}
+
+function math_div() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    if (b == 0) {
+        print "Error: Division by zero"
+        return
+    }
+    a = pop()
+    push(int(a / b))
+}
+
+function stack_print() {
+    if (stack_ptr < 1) {
+        print "Error: Stack underflow"
+        return
+    }
+    print pop()
+}
+
+function stack_show() {
+    print "<", stack_ptr, "> "
+    for (i = 0; i < stack_ptr; i++) {
+        printf "%s ", stack[i]
+    }
+    print ""
+    # print "Stack state after .s: "
+    # for (i = 0; i < stack_ptr; i++) {
+    #     print stack[i]
+    # }
+    # print ""
+}
+
+function stack_dup() {
+    if (stack_ptr < 1) {
+        print "Error: Stack underflow"
+        return
+    }
+    val = stack[stack_ptr - 1]
+    push(val)
+}
+
+function stack_drop() {
+    if (stack_ptr < 1) {
+        print "Error: Stack underflow"
+        return
+    }
+    pop()
+}
+
+function stack_swap() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(b)
+    push(a)
+}
+
+function stack_over() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a)
+    push(b)
+    push(a)
+}
+
+function stack_rot() {
+    if (stack_ptr < 3) {
+        print "Error: Stack underflow"
+        return
+    }
+    c = pop()
+    b = pop()
+    a = pop()
+    push(b)
+    push(c)
+    push(a)
+}
+
+function compare_eq() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a == b ? -1 : 0)
+}
+
+function compare_lt() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a < b ? -1 : 0)
+}
+
+function compare_gt() {
+    if (stack_ptr < 2) {
+        print "Error: Stack underflow"
+        return
+    }
+    b = pop()
+    a = pop()
+    push(a > b ? -1 : 0)
+}
+
+function exit_program() {
+    print "Exiting program."
+    exit 0
+}
+
+function list_words() {
+    print "Available words:"
+    
+    # Separate arrays to hold built-in and user-defined words
+    split("", built_in_words)
+    split("", user_defined_words)
+    
+    for (w in dict) {
+        split(dict[w], parts, ": ")
+        if (parts[1] ~ /^word /) {
+            user_defined_words[w] = parts[2]
+        } else {
+            built_in_words[w] = parts[2]
+        }
+    }
+    
+    # Sort built-in words manually because I'm picky
+    n = 0
+    for (w in built_in_words) {
+        sorted_words[n++] = w
+    }
+    
+    for (i = 0; i < n; i++) {
+        for (j = i + 1; j < n; j++) {
+            if (sorted_words[i] > sorted_words[j]) {
+                temp = sorted_words[i]
+                sorted_words[i] = sorted_words[j]
+                sorted_words[j] = temp
+            }
+        }
+    }
+    
+    # First print the built-in words
+    for (i = 0; i < n; i++) {
+        print sorted_words[i] ": " built_in_words[sorted_words[i]]
+    }
+    
+    # Then print the user-defined words
+    for (w in user_defined_words) {
+        print w ": " user_defined_words[w] " ( User-defined )"
+    }
+}
\ No newline at end of file
diff --git a/awk/forth/old/f.awk b/awk/forth/old/f.awk
new file mode 100755
index 0000000..eed9774
--- /dev/null
+++ b/awk/forth/old/f.awk
@@ -0,0 +1,344 @@
+#!/usr/bin/awk -f
+
+# Forth interpreter in AWK
+
+BEGIN {
+    print "Welcome to the AWK Forth Interpreter!"
+    print "Type your commands below. Use 'bye' to quit."
+    # Initialize variables
+    top = -1  # Initialize stack pointer
+    
+    # Initialize the dictionary with basic words
+    words["+"] = "+"
+    words["-"] = "-"
+    words["*"] = "*"
+    words["/"] = "/"
+    words["dup"] = "dup"
+    words["over"] = "over"
+    words["swap"] = "swap"
+    words["."] = "."
+    words["bye"] = "bye"
+    words["rot"] = "rot"
+    words["drop"] = "drop"
+    words["nip"] = "nip"
+    words["tuck"] = "tuck"
+    words["roll"] = "roll"
+    words["pick"] = "pick"
+    words["negate"] = "negate"
+    words["abs"] = "abs"
+    words["max"] = "max"
+    words["min"] = "min"
+    words["mod"] = "mod"
+    words["="] = "="
+    words["see"] = "see"
+    words["if"] = "if"
+    words["then"] = "then"
+    words["else"] = "else"
+    words[">"] = ">"
+    words["<"] = "<"
+    
+    # Add handlers for all words
+    handlers["+"] = "add"
+    handlers["-"] = "subtract"
+    handlers["*"] = "multiply"
+    handlers["/"] = "divide"
+    handlers["dup"] = "dup"
+    handlers["over"] = "over"
+    handlers["swap"] = "swap"
+    handlers["."] = "print_top"
+    handlers["<"] = "less_than"
+    handlers[">"] = "greater_than"
+    handlers["rot"] = "rot"
+    handlers["drop"] = "drop"
+    handlers["nip"] = "nip"
+    handlers["tuck"] = "tuck"
+    handlers["roll"] = "roll"
+    handlers["pick"] = "pick"
+    handlers["negate"] = "negate"
+    handlers["abs"] = "abs"
+    handlers["max"] = "max"
+    handlers["min"] = "min"
+    handlers["mod"] = "mod"
+    handlers["="] = "equals"
+    handlers["if"] = "handle_if"
+    handlers["then"] = "handle_then"
+    handlers["else"] = "handle_else"
+    handlers["bye"] = "bye"
+    handlers["see"] = "see"
+
+    # Add descriptions for words
+    desc["+"] = "( n1 n2 -- sum ) Add top two numbers"
+    desc["-"] = "( n1 n2 -- diff ) Subtract top number from second"
+    desc["*"] = "( n1 n2 -- prod ) Multiply top two numbers"
+    desc["/"] = "( n1 n2 -- quot ) Divide second by top"
+    desc["dup"] = "( n -- n n ) Duplicate top of stack"
+    desc["over"] = "( n1 n2 -- n1 n2 n1 ) Copy second item to top"
+    desc["swap"] = "( n1 n2 -- n2 n1 ) Swap top two items"
+    desc["rot"] = "( n1 n2 n3 -- n2 n3 n1 ) Rotate top three items"
+    desc["drop"] = "( n -- ) Discard top item"
+    desc["nip"] = "( n1 n2 -- n2 ) Remove second item"
+    desc["tuck"] = "( n1 n2 -- n2 n1 n2 ) Copy top item below second"
+    desc["roll"] = "( nk ... n1 n0 k -- nk-1 ... n1 n0 nk ) Move kth item to top"
+    desc["pick"] = "( nk ... n1 n0 k -- nk ... n1 n0 nk ) Copy kth item to top"
+    desc["negate"] = "( n -- -n ) Negate number"
+    desc["abs"] = "( n -- |n| ) Absolute value"
+    desc["max"] = "( n1 n2 -- max ) Maximum of top two numbers"
+    desc["min"] = "( n1 n2 -- min ) Minimum of top two numbers"
+    desc["mod"] = "( n1 n2 -- rem ) Remainder of n1/n2"
+    desc["="] = "( n1 n2 -- flag ) Test if equal, leaves 1 if true, 0 if false"
+    desc["if"] = "( flag -- ) Begin conditional execution"
+    desc["then"] = "( -- ) End conditional execution"
+    desc["else"] = "( -- ) Execute if previous condition was false"
+    desc[">"] = "( n1 n2 -- flag ) Returns true if n1 is greater than n2"
+    desc["<"] = "( n1 n2 -- flag ) Returns true if n1 is less than n2"
+    desc["bye"] = "( -- ) Exit the interpreter"
+    desc["see"] = "( -- ) Show definition of a word"
+
+    # Initialize condition stack
+    cond_top = -1
+
+    # Mark these as compile-only words
+    compile_only["if"] = 1
+    compile_only["then"] = 1
+    compile_only["else"] = 1
+}
+
+# Stack operations
+function push(value) {
+    stack[++top] = value
+}
+
+function pop() {
+    if (top < 0) {
+        print "Error: Stack underflow"
+        return 0
+    }
+    return stack[top--]
+}
+
+function check_stack(min_items, error_msg) {
+    if (top < min_items - 1) {
+        print error_msg ? error_msg : "Error: Not enough values on stack"
+        return 0
+    }
+    return 1
+}
+
+# Binary operations
+function binary_op(operation) {
+    if (!check_stack(2)) return
+    second = pop()
+    first = pop()
+    if (operation == "+") push(first + second)
+    else if (operation == "-") push(first - second)
+    else if (operation == "*") push(first * second)
+    else if (operation == "/") {
+        if (second == 0) {
+            print "Error: Division by zero"
+            push(first)
+            push(second)
+            return
+        }
+        push(first / second)
+    }
+    else if (operation == "mod") push(first % second)
+    else if (operation == "=") push(first == second ? 1 : 0)
+    else if (operation == "<") push(first < second ? 1 : 0)
+    else if (operation == ">") push(first > second ? 1 : 0)
+}
+
+# Handler functions
+function add() { binary_op("+") }
+function subtract() { binary_op("-") }
+function multiply() { binary_op("*") }
+function divide() { binary_op("/") }
+function mod() { binary_op("mod") }
+function equals() { binary_op("=") }
+function less_than() { binary_op("<") }
+function greater_than() { binary_op(">") }
+
+function dup() {
+    if (!check_stack(1)) return
+    push(stack[top])
+}
+
+function over() {
+    if (!check_stack(2)) return
+    push(stack[top - 1])
+}
+
+function swap() {
+    if (!check_stack(2)) return
+    temp = pop()
+    second = pop()
+    push(temp)
+    push(second)
+}
+
+function rot() {
+    if (!check_stack(3)) return
+    third = pop()
+    second = pop()
+    first = pop()
+    push(second)
+    push(third)
+    push(first)
+}
+
+function drop() {
+    if (!check_stack(1)) return
+    top--
+}
+
+function nip() {
+    if (!check_stack(2)) return
+    temp = stack[top]
+    drop()
+    drop()
+    push(temp)
+}
+
+function tuck() {
+    if (!check_stack(2)) return
+    temp = pop()
+    second = pop()
+    push(temp)
+    push(second)
+    push(temp)
+}
+
+function roll() {
+    if (!check_stack(1)) return
+    n = int(pop())
+    if (!check_stack(n)) return
+    if (n <= 0) return
+
+    temp = stack[top - n + 1]
+    for (i = top - n + 1; i < top; i++) {
+        stack[i] = stack[i + 1]
+    }
+    stack[top] = temp
+}
+
+function pick() {
+    if (!check_stack(1)) return
+    n = int(pop())
+    if (!check_stack(n)) return
+    if (n < 0) return
+    push(stack[top - n])
+}
+
+function negate() {
+    if (!check_stack(1)) return
+    push(-pop())
+}
+
+function abs() {
+    if (!check_stack(1)) return
+    n = pop()
+    push(n < 0 ? -n : n)
+}
+
+function max() {
+    if (!check_stack(2)) return
+    b = pop()
+    a = pop()
+    push(a > b ? a : b)
+}
+
+function min() {
+    if (!check_stack(2)) return
+    b = pop()
+    a = pop()
+    push(a < b ? a : b)
+}
+
+function print_top() {
+    if (!check_stack(1)) return
+    print stack[top]
+    drop()
+}
+
+function bye() {
+    exit
+}
+
+function see(word) {
+    if (!(word in words)) {
+        print "Error: Word '" word "' not found"
+        return
+    }
+    if (word in desc) {
+        print desc[word]
+    }
+    if (word in raw_definitions) {
+        print ": " word " " raw_definitions[word] " ;"
+    }
+}
+
+# Main processing function
+function execute_word(word) {
+    if (word in handlers) {
+        handler = handlers[word]
+        if (handler == "bye") exit
+        else if (handler == "see") {
+            if (i + 1 <= NF) see($(++i))
+            else print "Error: see requires a word name"
+        }
+        else if (handler == "add") add()
+        else if (handler == "subtract") subtract()
+        else if (handler == "multiply") multiply()
+        else if (handler == "divide") divide()
+        else if (handler == "dup") dup()
+        else if (handler == "over") over()
+        else if (handler == "swap") swap()
+        else if (handler == "print_top") print_top()
+        else if (handler == "less_than") less_than()
+        else if (handler == "greater_than") greater_than()
+        else if (handler == "rot") rot()
+        else if (handler == "drop") drop()
+        else if (handler == "nip") nip()
+        else if (handler == "tuck") tuck()
+        else if (handler == "roll") roll()
+        else if (handler == "pick") pick()
+        else if (handler == "negate") negate()
+        else if (handler == "abs") abs()
+        else if (handler == "max") max()
+        else if (handler == "min") min()
+        else if (handler == "mod") mod()
+        else if (handler == "equals") equals()
+        else if (handler == "handle_if") handle_if()
+        else if (handler == "handle_then") handle_then()
+        else if (handler == "handle_else") handle_else()
+        else {
+            print "Error: Handler '" handler "' not implemented"
+            return 0
+        }
+        return 1
+    }
+    return 0
+}
+
+# Process each line of input
+{
+    if (NF > 0) {
+        # Remove comments and normalize whitespace
+        gsub(/\(.*\)/, "")
+        gsub(/^[[:space:]]+/, "")
+        gsub(/[[:space:]]+$/, "")
+        gsub(/[[:space:]]+/, " ")
+
+        # Process each token
+        for (i = 1; i <= NF; i++) {
+            if ($i ~ /^-?[0-9]+$/) {
+                push($i)
+            } else if ($i in words) {
+                if (!execute_word($i)) {
+                    print "Error: Failed to execute word '" $i "'"
+                }
+            } else {
+                print "Error: Unknown word '" $i "'"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/awk/forth/old/test.forth b/awk/forth/old/test.forth
new file mode 100644
index 0000000..a1f4f50
--- /dev/null
+++ b/awk/forth/old/test.forth
@@ -0,0 +1,44 @@
+( Basic arithmetic operations )
+2 3 + . ( expect: 5 )
+10 3 - . ( expect: 7 )
+4 5 * . ( expect: 20 )
+20 4 / . ( expect: 5 )
+7 3 mod . ( expect: 1 )
+
+( Stack manipulation operations )
+5 dup . . ( expect: 5 5 )
+1 2 swap . . ( expect: 2 1 )
+1 2 over . . . ( expect: 1 2 1 )
+1 2 3 rot . . . ( expect: 2 3 1 )
+1 2 3 4 2 roll . . . . ( expect: 1 3 4 2 )
+5 drop
+1 2 nip . ( expect: 2 )
+1 2 tuck . . . ( expect: 2 1 2 )
+
+( Comparison operations )
+5 3 > . ( expect: 1 )
+3 5 < . ( expect: 1 )
+4 4 = . ( expect: 1 )
+5 3 < . ( expect: 0 )
+3 5 > . ( expect: 0 )
+4 5 = . ( expect: 0 )
+
+( Math operations )
+5 negate . ( expect: -5 )
+-7 abs . ( expect: 7 )
+5 2 max . ( expect: 5 )
+5 2 min . ( expect: 2 )
+
+( Complex stack manipulations )
+1 2 3 4 5 \ Put 5 numbers on stack
+3 pick . ( expect: 2 )
+2 roll . ( expect: 4 )
+. . . . ( expect: 5 3 1 )
+
+( Error handling tests )
+drop drop drop drop drop \ Clear stack
+drop ( expect: Error: Stack underflow )
+. ( expect: Error: Stack underflow )
+5 0 / ( expect: Error: Division by zero )
+
+bye
\ No newline at end of file
diff --git a/awk/forth/test.forth b/awk/forth/test.forth
new file mode 100644
index 0000000..daa6943
--- /dev/null
+++ b/awk/forth/test.forth
@@ -0,0 +1,34 @@
+\ Test arithmetic operations
+10 5 + .          \ Should print 15
+10 5 - .          \ Should print 5
+10 5 * .          \ Should print 50
+10 5 / .          \ Should print 2
+
+\ Test stack manipulation
+1 2 3 .s          \ Should show 3 values: 1 2 3
+dup .             \ Should print 3 again
+drop .            \ Should print 2
+swap .s           \ Should show 2 1
+over .s           \ Should show 2 1 2
+rot .s            \ Should show 1 2 3
+
+\ Test comparisons
+5 5 = .           \ Should print -1 (true)
+5 3 < .          \ Should print 0 (false)
+3 5 > .          \ Should print 0 (false)
+
+\ Test conditionals within user-defined words
+: test_if 10 20 if .s then ;  \ Should print 1 2 (since the condition is true)
+: test_else 10 5 if .s else 1 then ;  \ Should print 1 (since the condition is false)
+
+\ Test user-defined words
+: square dup * ;   \ Define a word to square a number
+4 square .         \ Should print 16
+
+: add_three 1 2 + + ;  \ Define a word to add three numbers
+1 2 add_three .    \ Should print 6
+
+\ List all words
+words              \ Should list all available words
+
+bye                \ Exit the interpreter 
\ No newline at end of file
diff --git a/awk/retro/retro.awk b/awk/retro/retro.awk
new file mode 100755
index 0000000..2a14ff0
--- /dev/null
+++ b/awk/retro/retro.awk
@@ -0,0 +1,250 @@
+#!/usr/bin/awk -f
+
+# Constants and VM setup
+BEGIN {
+    IMAGE_SIZE = 524288    # Amount of simulated RAM
+    DATA_DEPTH = 8192      # Depth of data stack
+    ADDRESS_DEPTH = 32768  # Depth of the stacks
+    
+    # Initialize stacks
+    data_sp = 0
+    addr_sp = 0
+    
+    # VM state
+    ip = 0
+    
+    # Opcode definitions
+    OP_NOP = 0
+    OP_LIT = 1
+    OP_DUP = 2
+    OP_DROP = 3
+    OP_SWAP = 4
+    OP_PUSH = 5
+    OP_POP = 6
+    OP_JUMP = 7
+    OP_CALL = 8
+    OP_CCALL = 9
+    OP_RETURN = 10
+    OP_EQ = 11
+    OP_NEQ = 12
+    OP_LT = 13
+    OP_GT = 14
+    OP_FETCH = 15
+    OP_STORE = 16
+    OP_ADD = 17
+    OP_SUB = 18
+    OP_MUL = 19
+    OP_DIVMOD = 20
+    OP_AND = 21
+    OP_OR = 22
+    OP_XOR = 23
+    OP_SHIFT = 24
+    OP_ZERO_EXIT = 25
+    OP_HALT = 26
+    
+    # Initialize VM
+    prepare_vm()
+    
+    # Load and run test program
+    load_test_program()
+    execute(0)
+    
+    # Print results
+    print "Stack contents after execution:"
+    print_stack()
+}
+
+# Stack operations
+function stack_push(stack_name, value) {
+    if (stack_name == "data") {
+        data_sp++
+        data_stack[data_sp] = value
+    } else if (stack_name == "addr") {
+        addr_sp++
+        addr_stack[addr_sp] = value
+    }
+}
+
+function stack_pop(stack_name) {
+    if (stack_name == "data") {
+        if (data_sp > 0) {
+            value = data_stack[data_sp]
+            data_sp--
+            return value
+        }
+    } else if (stack_name == "addr") {
+        if (addr_sp > 0) {
+            value = addr_stack[addr_sp]
+            addr_sp--
+            return value
+        }
+    }
+    return 0
+}
+
+function stack_tos(stack_name) {
+    if (stack_name == "data" && data_sp > 0) {
+        return data_stack[data_sp]
+    }
+    return 0
+}
+
+function stack_nos(stack_name) {
+    if (stack_name == "data" && data_sp > 1) {
+        return data_stack[data_sp - 1]
+    }
+    return 0
+}
+
+# Bitwise operations
+function bitwise_and(x, y,    i, result, a, b) {
+    result = 0
+    for (i = 0; i < 32; i++) {
+        a = int(x / (2 ^ i)) % 2
+        b = int(y / (2 ^ i)) % 2
+        if (a == 1 && b == 1)
+            result += 2 ^ i
+    }
+    return result
+}
+
+function bitwise_or(x, y,    i, result, a, b) {
+    result = 0
+    for (i = 0; i < 32; i++) {
+        a = int(x / (2 ^ i)) % 2
+        b = int(y / (2 ^ i)) % 2
+        if (a == 1 || b == 1)
+            result += 2 ^ i
+    }
+    return result
+}
+
+function bitwise_xor(x, y,    i, result, a, b) {
+    result = 0
+    for (i = 0; i < 32; i++) {
+        a = int(x / (2 ^ i)) % 2
+        b = int(y / (2 ^ i)) % 2
+        if (a != b)
+            result += 2 ^ i
+    }
+    return result
+}
+
+# Helper functions
+function abs(x) {
+    return x < 0 ? -x : x
+}
+
+function lshift(x, n) {
+    return int(x * (2 ^ n))
+}
+
+function rshift(x, n) {
+    return int(x / (2 ^ n))
+}
+
+# VM core functions
+function process_opcode(opcode) {
+    if (opcode == OP_NOP) {
+        return
+    }
+    else if (opcode == OP_LIT) {
+        ip++
+        stack_push("data", image[ip])
+    }
+    else if (opcode == OP_DUP) {
+        stack_push("data", stack_tos("data"))
+    }
+    else if (opcode == OP_DROP) {
+        stack_pop("data")
+    }
+    else if (opcode == OP_SWAP) {
+        temp = stack_pop("data")
+        temp2 = stack_pop("data")
+        stack_push("data", temp)
+        stack_push("data", temp2)
+    }
+    else if (opcode == OP_ADD) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", x + y)
+    }
+    else if (opcode == OP_SUB) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", y - x)
+    }
+    else if (opcode == OP_MUL) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", x * y)
+    }
+    else if (opcode == OP_HALT) {
+        ip = IMAGE_SIZE
+    }
+}
+
+function check_stack() {
+    if (data_sp < 0 || addr_sp < 0 || 
+        data_sp > DATA_DEPTH || addr_sp > ADDRESS_DEPTH) {
+        ip = 0
+        data_sp = 0
+        addr_sp = 0
+    }
+}
+
+function process_packed_opcodes(packed) {
+    ops[0] = bitwise_and(packed, 255)
+    ops[1] = bitwise_and(rshift(packed, 8), 255)
+    ops[2] = bitwise_and(rshift(packed, 16), 255)
+    ops[3] = bitwise_and(rshift(packed, 24), 255)
+    
+    for (i = 0; i < 4; i++) {
+        if (ops[i] != 0) {
+            process_opcode(ops[i])
+        }
+    }
+}
+
+function execute(offset) {
+    addr_sp = 1
+    ip = offset
+    
+    while (ip < IMAGE_SIZE) {
+        opcode = image[ip]
+        process_packed_opcodes(opcode)
+        
+        if (addr_sp == 0)
+            ip = IMAGE_SIZE
+            
+        ip++
+    }
+}
+
+function prepare_vm() {
+    ip = 0
+    data_sp = 0
+    addr_sp = 0
+}
+
+# Test program loader
+function pack_opcodes(op1, op2, op3, op4) {
+    return op1 + (op2 * 256) + (op3 * 65536) + (op4 * 16777216)
+}
+
+function load_test_program() {
+    # Simple test program that adds 10 and 5
+    image[0] = pack_opcodes(OP_LIT, 0, 0, 0)  # Push literal
+    image[1] = 10                             # Value 10
+    image[2] = pack_opcodes(OP_LIT, 0, 0, 0)  # Push literal
+    image[3] = 5                              # Value 5
+    image[4] = pack_opcodes(OP_ADD, 0, 0, 0)  # Add them
+    image[5] = pack_opcodes(OP_HALT, 0, 0, 0) # Halt
+}
+
+# Debug helper
+function print_stack() {
+    for (i = 1; i <= data_sp; i++) {
+        print "Item", i ":", data_stack[i]
+    }
+}
\ No newline at end of file
diff --git a/awk/retro/test.awk b/awk/retro/test.awk
new file mode 100755
index 0000000..191fa5b
--- /dev/null
+++ b/awk/retro/test.awk
@@ -0,0 +1,52 @@
+#!/usr/bin/awk -f
+
+@include "vm.awk"
+
+# Complex test program
+BEGIN {
+    # Test program to calculate factorial of 5
+    i = 0
+    
+    # Push 5 onto stack
+    image[i++] = pack_opcodes(OP_LIT, 0, 0, 0)
+    image[i++] = 5
+    
+    # Push 1 onto stack (accumulator)
+    image[i++] = pack_opcodes(OP_LIT, 0, 0, 0)
+    image[i++] = 1
+    
+    # Start of multiplication loop
+    loop_start = i
+    
+    # Duplicate top number (counter)
+    image[i++] = pack_opcodes(OP_DUP, 0, 0, 0)
+    
+    # Test if counter is zero
+    image[i++] = pack_opcodes(OP_ZERO_EXIT, 0, 0, 0)
+    
+    # Multiply accumulator by counter
+    image[i++] = pack_opcodes(OP_MUL, 0, 0, 0)
+    
+    # Decrement counter
+    image[i++] = pack_opcodes(OP_LIT, 0, 0, 0)
+    image[i++] = 1
+    image[i++] = pack_opcodes(OP_SUB, 0, 0, 0)
+    
+    # Jump back to start of loop
+    image[i++] = pack_opcodes(OP_LIT, 0, 0, 0)
+    image[i++] = loop_start
+    image[i++] = pack_opcodes(OP_JUMP, 0, 0, 0)
+    
+    # Halt
+    image[i++] = pack_opcodes(OP_HALT, 0, 0, 0)
+    
+    # Execute program
+    execute(0)
+    
+    # Print result (should be 120 - factorial of 5)
+    print "Factorial of 5:", stack_tos("data")
+}
+
+function pack_opcodes(op1, op2, op3, op4) {
+    return op1 + (op2 * 256) + (op3 * 65536) + (op4 * 16777216)
+}
\ No newline at end of file
diff --git a/awk/retro/vm.awk b/awk/retro/vm.awk
new file mode 100755
index 0000000..cd894c5
--- /dev/null
+++ b/awk/retro/vm.awk
@@ -0,0 +1,364 @@
+#!/usr/bin/awk -f
+
+# Constants
+BEGIN {
+    IMAGE_SIZE = 524288    # Amount of simulated RAM
+    DATA_DEPTH = 8192      # Depth of data stack
+    ADDRESS_DEPTH = 32768  # Depth of the stacks
+    
+    # Initialize stacks
+    data_sp = 0
+    addr_sp = 0
+    
+    # VM state
+    ip = 0
+    
+    # Opcode definitions
+    OP_NOP = 0
+    OP_LIT = 1
+    OP_DUP = 2
+    OP_DROP = 3
+    OP_SWAP = 4
+    OP_PUSH = 5
+    OP_POP = 6
+    OP_JUMP = 7
+    OP_CALL = 8
+    OP_CCALL = 9
+    OP_RETURN = 10
+    OP_EQ = 11
+    OP_NEQ = 12
+    OP_LT = 13
+    OP_GT = 14
+    OP_FETCH = 15
+    OP_STORE = 16
+    OP_ADD = 17
+    OP_SUB = 18
+    OP_MUL = 19
+    OP_DIVMOD = 20
+    OP_AND = 21
+    OP_OR = 22
+    OP_XOR = 23
+    OP_SHIFT = 24
+    OP_ZERO_EXIT = 25
+    OP_HALT = 26
+    OP_IE = 27
+    OP_IQ = 28
+    OP_II = 29
+}
+
+# Stack operations
+function stack_push(stack_name, value) {
+    if (stack_name == "data") {
+        data_sp++
+        data_stack[data_sp] = value
+    } else if (stack_name == "addr") {
+        addr_sp++
+        addr_stack[addr_sp] = value
+    }
+}
+
+function stack_pop(stack_name) {
+    if (stack_name == "data") {
+        if (data_sp > 0) {
+            value = data_stack[data_sp]
+            data_sp--
+            return value
+        }
+    } else if (stack_name == "addr") {
+        if (addr_sp > 0) {
+            value = addr_stack[addr_sp]
+            addr_sp--
+            return value
+        }
+    }
+    return 0
+}
+
+function stack_tos(stack_name) {
+    if (stack_name == "data" && data_sp > 0) {
+        return data_stack[data_sp]
+    }
+    return 0
+}
+
+function stack_nos(stack_name) {
+    if (stack_name == "data" && data_sp > 1) {
+        return data_stack[data_sp - 1]
+    }
+    return 0
+}
+
+# Bitwise operation implementations
+function bitwise_and(x, y,    i, result, a, b) {
+    result = 0
+    for (i = 0; i < 32; i++) {
+        a = int(x / (2 ^ i)) % 2
+        b = int(y / (2 ^ i)) % 2
+        if (a == 1 && b == 1)
+            result += 2 ^ i
+    }
+    return result
+}
+
+function bitwise_or(x, y,    i, result, a, b) {
+    result = 0
+    for (i = 0; i < 32; i++) {
+        a = int(x / (2 ^ i)) % 2
+        b = int(y / (2 ^ i)) % 2
+        if (a == 1 || b == 1)
+            result += 2 ^ i
+    }
+    return result
+}
+
+function bitwise_xor(x, y,    i, result, a, b) {
+    result = 0
+    for (i = 0; i < 32; i++) {
+        a = int(x / (2 ^ i)) % 2
+        b = int(y / (2 ^ i)) % 2
+        if (a != b)
+            result += 2 ^ i
+    }
+    return result
+}
+
+function lshift(x, n) {
+    return int(x * (2 ^ n))
+}
+
+function rshift(x, n) {
+    return int(x / (2 ^ n))
+}
+
+# VM instruction implementations
+function process_opcode(opcode) {
+    if (opcode == OP_NOP) {
+        return
+    }
+    else if (opcode == OP_LIT) {
+        ip++
+        stack_push("data", image[ip])
+    }
+    else if (opcode == OP_DUP) {
+        stack_push("data", stack_tos("data"))
+    }
+    else if (opcode == OP_DROP) {
+        stack_pop("data")
+    }
+    else if (opcode == OP_SWAP) {
+        temp = stack_pop("data")
+        temp2 = stack_pop("data")
+        stack_push("data", temp)
+        stack_push("data", temp2)
+    }
+    else if (opcode == OP_PUSH) {
+        stack_push("addr", stack_pop("data"))
+    }
+    else if (opcode == OP_POP) {
+        stack_push("data", stack_pop("addr"))
+    }
+    else if (opcode == OP_JUMP) {
+        ip = stack_pop("data") - 1
+    }
+    else if (opcode == OP_CALL) {
+        stack_push("addr", ip)
+        ip = stack_pop("data") - 1
+    }
+    else if (opcode == OP_CCALL) {
+        a = stack_pop("data")
+        b = stack_pop("data")
+        if (b != 0) {
+            stack_push("addr", ip)
+            ip = a - 1
+        }
+    }
+    else if (opcode == OP_RETURN) {
+        ip = stack_pop("addr")
+    }
+    else if (opcode == OP_EQ) {
+        a = stack_pop("data")
+        b = stack_pop("data")
+        stack_push("data", (b == a) ? -1 : 0)
+    }
+    else if (opcode == OP_NEQ) {
+        a = stack_pop("data")
+        b = stack_pop("data")
+        stack_push("data", (b != a) ? -1 : 0)
+    }
+    else if (opcode == OP_LT) {
+        a = stack_pop("data")
+        b = stack_pop("data")
+        stack_push("data", (b < a) ? -1 : 0)
+    }
+    else if (opcode == OP_GT) {
+        a = stack_pop("data")
+        b = stack_pop("data")
+        stack_push("data", (b > a) ? -1 : 0)
+    }
+    else if (opcode == OP_FETCH) {
+        x = stack_pop("data")
+        if (x == -1)
+            stack_push("data", data_sp)
+        else if (x == -2)
+            stack_push("data", addr_sp)
+        else if (x == -3)
+            stack_push("data", IMAGE_SIZE)
+        else if (x == -4)
+            stack_push("data", -2147483648)
+        else if (x == -5)
+            stack_push("data", 2147483647)
+        else
+            stack_push("data", image[x])
+    }
+    else if (opcode == OP_STORE) {
+        addr = stack_pop("data")
+        value = stack_pop("data")
+        image[addr] = value
+    }
+    else if (opcode == OP_ADD) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", x + y)
+    }
+    else if (opcode == OP_SUB) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", y - x)
+    }
+    else if (opcode == OP_MUL) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", y * x)
+    }
+    else if (opcode == OP_DIVMOD) {
+        b = stack_pop("data")
+        a = stack_pop("data")
+        if (b == 0) {
+            ip = 0
+            data_sp = 0
+            addr_sp = 0
+        } else {
+            x = abs(b)
+            y = abs(a)
+            q = int(y / x)
+            r = y % x
+            if (a < 0 && b < 0)
+                r = r * -1
+            if (a > 0 && b < 0)
+                q = q * -1
+            if (a < 0 && b > 0) {
+                r = r * -1
+                q = q * -1
+            }
+            stack_push("data", r)
+            stack_push("data", q)
+        }
+    }
+    else if (opcode == OP_AND) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", bitwise_and(x, y))
+    }
+    else if (opcode == OP_OR) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", bitwise_or(x, y))
+    }
+    else if (opcode == OP_XOR) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        stack_push("data", bitwise_xor(x, y))
+    }
+    else if (opcode == OP_SHIFT) {
+        x = stack_pop("data")
+        y = stack_pop("data")
+        if (x < 0)
+            stack_push("data", lshift(y, -x))
+        else
+            stack_push("data", rshift(y, x))
+    }
+    else if (opcode == OP_ZERO_EXIT) {
+        if (stack_tos("data") == 0) {
+            stack_pop("data")
+            ip = stack_pop("addr")
+        }
+    }
+    else if (opcode == OP_HALT) {
+        ip = IMAGE_SIZE
+    }
+    
+    check_stack()
+}
+
+# Helper functions
+function abs(x) {
+    return x < 0 ? -x : x
+}
+
+function check_stack() {
+    if (data_sp < 0 || addr_sp < 0 || 
+        data_sp > DATA_DEPTH || addr_sp > ADDRESS_DEPTH) {
+        ip = 0
+        data_sp = 0
+        addr_sp = 0
+    }
+}
+
+function process_packed_opcodes(packed) {
+    ops[0] = bitwise_and(packed, 255)
+    ops[1] = bitwise_and(rshift(packed, 8), 255)
+    ops[2] = bitwise_and(rshift(packed, 16), 255)
+    ops[3] = bitwise_and(rshift(packed, 24), 255)
+    
+    for (i = 0; i < 4; i++) {
+        if (ops[i] != 0) {
+            process_opcode(ops[i])
+        }
+    }
+}
+
+# Main execution function
+function execute(offset) {
+    addr_sp = 1
+    ip = offset
+    
+    while (ip < IMAGE_SIZE) {
+        opcode = image[ip]
+        process_packed_opcodes(opcode)
+        
+        if (addr_sp == 0)
+            ip = IMAGE_SIZE
+            
+        ip++
+    }
+}
+
+# String handling functions
+function string_inject(str, buffer,    i, len) {
+    len = length(str)
+    for (i = 1; i <= len; i++) {
+        image[buffer + i - 1] = ord(substr(str, i, 1))
+        image[buffer + i] = 0
+    }
+}
+
+function string_extract(at,    str, i) {
+    str = ""
+    i = at
+    while (image[i] != 0) {
+        str = str chr(image[i])
+        i++
+    }
+    return str
+}
+
+# Initialize VM
+BEGIN {
+    prepare_vm()
+}
+
+function prepare_vm() {
+    ip = 0
+    data_sp = 0
+    addr_sp = 0
+}
\ No newline at end of file
diff --git a/awk/scheme/s.awk b/awk/scheme/s.awk
new file mode 100755
index 0000000..7c8bba6
--- /dev/null
+++ b/awk/scheme/s.awk
@@ -0,0 +1,139 @@
+#!/usr/bin/awk -f
+
+# Set debug mode
+DEBUG = 1  # Change to 0 to disable debug output
+
+# Environment to store variable bindings
+BEGIN {
+    print "Welcome to the AWK Scheme Interpreter!"
+    print "Type your Scheme expressions below (type 'exit' to quit):"
+    while (1) {
+        printf "> "
+        if (getline input <= 0) {
+            print "Error reading input. Exiting."
+            break
+        }
+        if (input == "exit") {
+            print "Exiting the interpreter."
+            exit
+        }
+        if (input == "") {
+            print "Empty input received, continuing..."
+            continue
+        }
+        
+        print "Input received: " input  # Echo the input
+        ast = parse(input)  # Parse the input
+        
+        # Print the entire AST for debugging
+        for (i = 1; i <= length(ast); i++) {
+            print "AST[" i "] = " ast[i]
+        }
+        
+        # Evaluate the AST
+        if (length(ast) > 0) {
+            result = eval(ast)  # Evaluate the AST
+            print "Result: " result  # Print the result
+        } else {
+            print "Parsed AST is empty."
+        }
+    }
+}
+
+# Function to parse input into an AST
+function parse(input) {
+    # Remove outer whitespace
+    gsub(/^\s+|\s+$/, "", input)
+    
+    # Check if input is empty after trimming
+    if (input == "") {
+        print "Input is empty after trimming"
+        return ""
+    }
+    
+    # Debugging: Print input before processing
+    print "Debug: Raw input for parsing: " input
+    
+    # Remove parentheses at start and end
+    if (substr(input, 1, 1) == "(") {
+        input = substr(input, 2)
+    }
+    if (substr(input, length(input), 1) == ")") {
+        input = substr(input, 1, length(input) - 1)
+    }
+    
+    # Debugging: Print input after removing outer parentheses
+    print "Debug: Input after removing outer parentheses: " input
+    
+    # Split the input into tokens
+    gsub(/\(/, " ( ", input)
+    gsub(/\)/, " ) ", input)
+    gsub(/\s+/, " ", input)  # normalize whitespace
+    gsub(/^\s+|\s+$/, "", input)  # trim
+    
+    # Debugging: Print input after tokenization
+    print "Debug: Input after tokenization: " input
+    
+    n = split(input, ast, " ")
+    
+    # Debugging: Print the number of tokens
+    print "Debug: Number of tokens: " n
+    
+    return ast
+}
+
+# Function to evaluate the AST
+function eval(ast,    i, result) {
+    # Debugging: Print the current AST being evaluated
+    print "Debug: Evaluating AST: " ast[1] " " ast[2] " " ast[3]
+    
+    # Handle numbers directly
+    if (ast[1] ~ /^[+-]?[0-9]+$/) {
+        print "Debug: Returning number: " ast[1]
+        return ast[1] + 0  # Convert string to number
+    }
+    
+    # Handle addition
+    if (ast[1] == "+") {
+        result = 0
+        for (i = 2; i <= length(ast); i++) {
+            print "Debug: Adding operand: " ast[i]
+            result += eval(ast[i])  # Recursively evaluate operands
+        }
+        return result
+    }
+    
+    # Handle subtraction
+    if (ast[1] == "-") {
+        result = eval(ast[2])  # Start with the first operand
+        for (i = 3; i <= length(ast); i++) {
+            print "Debug: Subtracting operand: " ast[i]
+            result -= eval(ast[i])  # Subtract subsequent operands
+        }
+        return result
+    }
+    
+    # Handle multiplication
+    if (ast[1] == "*") {
+        result = 1
+        for (i = 2; i <= length(ast); i++) {
+            print "Debug: Multiplying operand: " ast[i]
+            result *= eval(ast[i])  # Multiply operands
+        }
+        return result
+    }
+    
+    # Handle division
+    if (ast[1] == "/") {
+        result = eval(ast[2])  # Start with the first operand
+        for (i = 3; i <= length(ast); i++) {
+            print "Debug: Dividing by operand: " ast[i]
+            result /= eval(ast[i])  # Divide by subsequent operands
+        }
+        return result
+    }
+    
+    # If we reach here, the operation is not recognized
+    return "Error: Unknown operation " ast[1]
+}
+
diff --git a/awk/vm/vm.awk b/awk/vm/vm.awk
new file mode 100755
index 0000000..e53bf1a
--- /dev/null
+++ b/awk/vm/vm.awk
@@ -0,0 +1,208 @@
+#!/usr/bin/awk -f
+
+
+# Stack: DROP, DUP, OVER, PUSH, POP
+# Math:	+ AND XOR NOT 2* 2/ multiply-step
+# Call:	JUMP CALL RETURN IF -IF
+# Loop:	NEXT UNEXT
+# Register:	A A! B!
+# Memory: @ ! @+ !+ @B !B @P !P
+# NO-OP: .
+
+
+BEGIN {
+    # Initialize VM state
+    stack_pointer = 0    # Points to next free position
+    pc = 0              # Program counter
+    MAX_STACK = 100     # Maximum stack size
+    MAX_MEM = 1000      # Memory size
+    
+    # Initialize registers
+    A = 0              # A register
+    B = 0              # B register
+    P = 0              # P register (auxiliary pointer)
+    
+    # Stack operations
+    split("", stack)   # Initialize stack array
+    split("", memory)  # Initialize memory array
+}
+
+# Stack operations
+function push(value) {
+    if (stack_pointer >= MAX_STACK) {
+        print "Stack overflow!" > "/dev/stderr"
+        exit 1
+    }
+    stack[stack_pointer++] = value
+}
+
+function pop() {
+    if (stack_pointer <= 0) {
+        print "Stack underflow!" > "/dev/stderr"
+        exit 1
+    }
+    return stack[--stack_pointer]
+}
+
+# Basic stack manipulation
+function op_drop() {
+    pop()
+}
+
+function op_dup() {
+    if (stack_pointer <= 0) {
+        print "Stack underflow on DUP!" > "/dev/stderr"
+        exit 1
+    }
+    push(stack[stack_pointer - 1])
+}
+
+function op_over() {
+    if (stack_pointer <= 1) {
+        print "Stack underflow on OVER!" > "/dev/stderr"
+        exit 1
+    }
+    push(stack[stack_pointer - 2])
+}
+
+# Basic arithmetic operations
+function op_add() {
+    b = pop()
+    a = pop()
+    push(a + b)
+}
+
+function op_and() {
+    b = pop()
+    a = pop()
+    # For now, we'll just multiply as a placeholder
+    # In a real implementation, we'd need to implement proper bitwise AND
+    push(a * b)
+}
+
+function op_xor() {
+    b = pop()
+    a = pop()
+    # For now, we'll just add as a placeholder
+    # In a real implementation, we'd need to implement proper bitwise XOR
+    push(a + b)
+}
+
+function op_not() {
+    a = pop()
+    # For now, we'll just negate as a placeholder
+    # In a real implementation, we'd need to implement proper bitwise NOT
+    push(-a - 1)
+}
+
+function op_2times() {
+    a = pop()
+    push(a * 2)
+}
+
+function op_2div() {
+    a = pop()
+    push(int(a / 2))
+}
+
+# Register operations
+function op_a() {
+    push(A)
+}
+
+function op_astore() {
+    A = pop()
+}
+
+function op_bstore() {
+    B = pop()
+}
+
+# Memory operations
+function op_fetch() {
+    addr = pop()
+    push(memory[addr])
+}
+
+function op_store() {
+    value = pop()
+    addr = pop()
+    memory[addr] = value
+}
+
+function op_fetchplus() {
+    push(memory[P++])
+}
+
+function op_storeplus() {
+    memory[P++] = pop()
+}
+
+function op_fetchb() {
+    push(memory[B])
+}
+
+function op_storeb() {
+    memory[B] = pop()
+}
+
+function op_fetchp() {
+    push(memory[P])
+}
+
+function op_storep() {
+    memory[P] = pop()
+}
+
+function print_stack() {
+    printf "Stack: "
+    for (i = 0; i < stack_pointer; i++) {
+        printf "%d ", stack[i]
+    }
+    printf "\n"
+}
+
+function execute_instruction(inst) {
+    if (inst ~ /^[0-9]+$/) {
+        # Numbers are pushed onto the stack
+        push(int(inst))
+        return
+    }
+    
+    if (inst == "BYE")      { exit 0 } # not really in the minimal spec as set out by Chuck Moore, but useful for a graceful exit.
+    if (inst == "DROP")     { op_drop(); return }
+    if (inst == "DUP")      { op_dup(); return }
+    if (inst == "OVER")     { op_over(); return }
+    if (inst == "+")        { op_add(); return }
+    if (inst == "AND")      { op_and(); return }
+    if (inst == "XOR")      { op_xor(); return }
+    if (inst == "NOT")      { op_not(); return }
+    if (inst == "2*")       { op_2times(); return } # multiply-step
+    if (inst == "2/")       { op_2div(); return } # divide-step
+    if (inst == "A")        { op_a(); return } # push A register
+    if (inst == "A!")       { op_astore(); return } # store A register
+    if (inst == "B!")       { op_bstore(); return } # store B register
+    if (inst == "@")        { op_fetch(); return } # fetch from memory
+    if (inst == "!")        { op_store(); return } # store to memory
+    if (inst == "@+")       { op_fetchplus(); return } # fetch from memory at P+
+    if (inst == "!+")       { op_storeplus(); return } # store to memory at P+
+    if (inst == "@B")       { op_fetchb(); return } # fetch from memory at B
+    if (inst == "!B")       { op_storeb(); return } # store to memory at B
+    if (inst == "@P")       { op_fetchp(); return } # fetch from memory at P
+    if (inst == "!P")       { op_storep(); return } # store to memory at P
+    if (inst == ".")        { return }  # NO-OP
+    
+    print "Unknown instruction: " inst > "/dev/stderr"
+    exit 1
+}
+
+# Main execution loop
+{
+    # Split the input line into words
+    n = split($0, words)
+    for (i = 1; i <= n; i++) {
+        execute_instruction(words[i])
+    }
+    # Print stack after each line of input
+    print_stack()
+}
diff --git a/awk/vm/vm_tests.sh b/awk/vm/vm_tests.sh
new file mode 100644
index 0000000..b3bfd6b
--- /dev/null
+++ b/awk/vm/vm_tests.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+echo "Running VM tests..."
+echo
+
+echo "Test 1: Basic register A operations"
+echo "42 A! A A" | awk -f vm.awk
+echo
+
+echo "Test 2: Register A and B with memory operations"
+echo "100 A! 200 B! 42 @B" | awk -f vm.awk
+echo
+
+echo "Test 3: Sequential memory operations using P register"
+echo "1 2 3 4 5 !+ !+ !+ !+ !+ 0 P! @+ @+ @+ @+ @+" | awk -f vm.awk
+echo
+
+echo "Test 4: Complex register manipulation"
+echo "42 A! A DUP B! @B" | awk -f vm.awk
+echo
+
+echo "Test 5: Register arithmetic"
+echo "5 A! 3 B! A B! @B A +" | awk -f vm.awk
+echo
+
+echo "Test 6: Memory pointer operations"
+echo "42 0 ! 1 P! @P" | awk -f vm.awk
+echo
+
+echo "Tests completed." 
\ No newline at end of file
diff --git a/chibi/pi.scm b/chibi/pi.scm
new file mode 100644
index 0000000..a32742c
--- /dev/null
+++ b/chibi/pi.scm
@@ -0,0 +1,17 @@
+(define (greory-leibniz-terms n)
+  (cond ((= n 0) '())
+        ((even? n) (cons 1/g (greory-leibniz-terms (+ (- n 1) /2))))
+        (else (cons (/(-1) (* 2 n +3)) (/(*x^2) x))))))
+
+(define pi-approximation 
+  (define x '())
+  (define f (lambda (y) y))
+
+  (display "Approximating Pi using Gregory-Leibniz series...\n")
+  (for-each
+    lambda (term)
+    (define n (car term))
+    (set! x (+ x (* 4 / n)))
+    (f (f (g (g (/(*f f 4)) (/(*x^2) x))))))))) ))
+
+(display pi-approximation))
\ No newline at end of file
diff --git a/html/cards/cards.js b/html/cards/cards.js
new file mode 100644
index 0000000..98aa0e1
--- /dev/null
+++ b/html/cards/cards.js
@@ -0,0 +1,441 @@
+/**
+ * @fileOverview Trying to make an easily extensible bit of code to handle
+ * creating and drawing any number of decks of cards. 
+ * 
+ * @author: eli_oat
+ * @license: no gods, no masters
+ */
+
+/**
+ * @typedef {Object} Card
+ * @property {number} x
+ * @property {number} y
+ * @property {CardData} card
+ * @property {boolean} isFaceUp
+ */
+
+/**
+ * @typedef {Object} CardData
+ * @property {string} suit
+ * @property {string} value
+ */
+
+/**
+ * @typedef {Object} GameState
+ * @property {Card[]} cards
+ * @property {Card|null} draggingCard
+ * @property {CardData[]} deck
+ * @property {{x: number, y: number}} stackPosition
+ */
+
+const CARD_WIDTH = 100;
+const CARD_HEIGHT = 150;
+const PADDING = 10;
+const SUITS = ['❀️', '♦️', '♣️', '♠️'];
+const VALUES = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
+const PATTERN_SIZE = 10;
+const INITIAL_CARD_X = 20;
+const INITIAL_CARD_Y = 20;
+const FONT_SIZE = '34px "pokemon-font", monospace';
+const CARD_BORDER_COLOR = '#000000';
+const CARD_FACE_COLOR = '#FFFFFF';
+const DECK_COUNT = 4; // Can be changed to any number
+const BASE_COLORS = [
+    { primary: '#FF9900', secondary: '#FFCC00' }, // Original orange deck
+    { primary: '#6B8E23', secondary: '#9ACD32' }, // Olive green deck
+    { primary: '#4169E1', secondary: '#87CEEB' }, // Royal blue deck
+    { primary: '#8B008B', secondary: '#DA70D6' }, // Purple deck
+    { primary: '#CD853F', secondary: '#DEB887' }  // Brown deck
+];
+
+
+// Pile layout
+const PILE_SPACING = CARD_WIDTH + PADDING * 4; // Space between piles
+const PILE_OFFSET = 5; // Vertical offset for stacked cards
+
+
+// Setting up the canvas
+const canvas = document.getElementById('cards');
+const ctx = canvas.getContext('2d');
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+
+/**
+ * Shuffles an array in place and returns a new shuffled array.
+ * @param {Array} array - The array to shuffle.
+ * @returns {Array} A new array containing the shuffled elements.
+ */
+const shuffle = array => {
+    const result = [...array];
+    for (let i = result.length - 1; i > 0; i--) {
+        const j = Math.floor(Math.random() * (i + 1));
+        [result[i], result[j]] = [result[j], result[i]];
+    }
+    return result;
+};
+
+
+/**
+ * Creates a deck of cards for a given deck index.
+ * @param {number} deckIndex - The index of the deck being created.
+ * @returns {Array} An array of card objects, each containing suit, value, and deckId.
+ */
+const createDeck = (deckIndex) => SUITS.flatMap(suit => 
+    VALUES.map(value => ({ 
+        suit, 
+        value,
+        deckId: deckIndex // Add deckId to track which deck a card belongs to
+    }))
+);
+
+/**
+ * Creates multiple decks of cards based on the specified count.
+ * If the count exceeds the number of unique deck colors defined, 
+ * some decks will repeat colors.
+ * 
+ * @param {number} count - The number of decks to create.
+ * @returns {Array} An array of card objects, each containing suit, value, and deckId.
+ */
+const createDecks = (count) => {
+    if (count > BASE_COLORS.length) {
+        console.warn(`Only ${BASE_COLORS.length} unique deck colors are defined. Some decks will repeat colors.`);
+    }
+    return Array.from({ length: count }, (_, i) => createDeck(i)).flat();
+};
+
+/**
+ * Creates a card object with a known position and some card data.
+ * @param {number} x - The x-coordinate of the card.
+ * @param {number} y - The y-coordinate of the card.
+ * @param {CardData} cardData - The data for the card, including suit and value.
+ * @returns {Card} A new card object.
+ */
+const createCard = (x, y, cardData) => Object.freeze({
+    x: x + PADDING,
+    y: y + PADDING,
+    card: Object.freeze({ ...cardData }),
+    isFaceUp: false
+});
+
+/**
+ * Determines if a point is within a card.
+ * Used to determine where to hold a card when dragging.
+ * @param {number} x - The x-coordinate of the point.
+ * @param {number} y - The y-coordinate of the point.
+ * @param {Card} card - The card to check by card object reference.
+ * @returns {boolean} True if the point is within the card.
+ */
+const isPointInCard = (x, y, card) =>
+    x >= card.x && x <= card.x + CARD_WIDTH && y >= card.y && y <= card.y + CARD_HEIGHT;
+
+const clearCanvas = () => {
+    ctx.fillStyle = 'beige';
+    ctx.fillRect(0, 0, canvas.width, canvas.height);
+};
+
+const drawCardBack = card => {
+    ctx.fillRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT);
+    drawRetroPattern(card);
+    ctx.strokeStyle = CARD_BORDER_COLOR;
+    ctx.strokeRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT);
+};
+
+const drawRetroPattern = card => {
+    const checkeredSize = 10;
+    const deckColors = BASE_COLORS[card.card.deckId % BASE_COLORS.length];
+    
+    for (let i = 0; i < CARD_WIDTH; i += checkeredSize) {
+        for (let j = 0; j < CARD_HEIGHT; j += checkeredSize) {
+            ctx.fillStyle = (Math.floor(i / checkeredSize) + Math.floor(j / checkeredSize)) % 2 === 0 
+                ? deckColors.primary 
+                : deckColors.secondary;
+            ctx.fillRect(card.x + i, card.y + j, checkeredSize, checkeredSize);
+        }
+    }
+};
+
+const drawCardFront = card => {
+    ctx.fillStyle = CARD_FACE_COLOR;
+    ctx.fillRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT);
+    ctx.fillStyle = CARD_BORDER_COLOR;
+    ctx.font = FONT_SIZE;
+    ctx.strokeRect(card.x, card.y, CARD_WIDTH, CARD_HEIGHT);
+    
+    drawCardValue(card.card.value, card.x + 12, card.y + 42, 'left');
+    drawCardSuit(card.card.suit, card.x + CARD_WIDTH / 2, card.y + CARD_HEIGHT / 2 + 20);
+};
+
+const drawCardValue = (value, x, y, alignment) => {
+    ctx.textAlign = alignment;
+    ctx.fillStyle = CARD_BORDER_COLOR;
+    ctx.fillText(value, x, y);
+};
+
+const drawCardSuit = (suit, x, y) => {
+    ctx.textAlign = 'center';
+    ctx.fillStyle = CARD_BORDER_COLOR;
+    ctx.fillText(suit, x, y);
+};
+
+/**
+ * Renders a card, determining which side to draw based on its face-up state.
+ * @param {Card} card - The card to render by card object reference.
+ */
+const renderCard = card => {
+    card.isFaceUp ? drawCardFront(card) : drawCardBack(card);
+};
+
+const renderAllCards = cards => {
+    clearCanvas();
+    cards.forEach(renderCard);
+};
+
+let gameState;
+
+const initializeGameState = () => ({
+    cards: [],
+    draggingCard: null,
+    deck: shuffle(createDecks(DECK_COUNT)),
+    stackPosition: { x: 0, y: 0 }
+});
+
+const initializeGame = () => {
+    try {
+        gameState = initializeGameState();
+        
+        // Group cards by deck
+        const cardsByDeck = gameState.deck.reduce((acc, cardData) => {
+            const deckId = cardData.deckId;
+            if (!acc[deckId]) acc[deckId] = [];
+            acc[deckId].push(cardData);
+            return acc;
+        }, {});
+
+        // Calculate starting X position to center all piles
+        // FIXME: How can I make the deck position be dynamic?
+        const totalWidth = PILE_SPACING * DECK_COUNT;
+        const startX = (canvas.width - totalWidth) / 2;
+
+        // Create cards for each deck in its own pile
+        gameState.cards = Object.entries(cardsByDeck).flatMap(([deckId, deckCards]) => {
+            const pileX = startX + (parseInt(deckId) * PILE_SPACING);
+            
+            return deckCards.map((cardData, indexInDeck) => 
+                createCard(
+                    pileX,
+                    INITIAL_CARD_Y + (indexInDeck * PILE_OFFSET),
+                    cardData
+                )
+            );
+        });
+
+        // TODO: Consider adding another level Box > Deck > Pile > Card
+
+        clearCanvas();
+        renderAllCards(gameState.cards);
+        setupEventListeners();
+
+    } catch (error) {
+        console.error('Failed to initialize game:', error);
+        alert('Failed to initialize game. Please refresh the page.');
+    }
+};
+
+const setupEventListeners = () => {
+    canvas.addEventListener('mousedown', handleMouseDown);
+    canvas.addEventListener('contextmenu', e => e.preventDefault());
+    document.addEventListener('keydown', e => {
+        if (e.key === 'q') handleResetGame();
+    });
+};
+
+const handleMouseMove = e => {
+    if (!gameState.draggingCard) return;
+
+    const rect = canvas.getBoundingClientRect();
+    const newX = e.clientX - rect.left - dragOffset.x;
+    const newY = e.clientY - rect.top - dragOffset.y;
+
+    const updatedCard = moveCard(gameState.draggingCard, newX, newY);
+    gameState.cards = gameState.cards.map(card => 
+        card === gameState.draggingCard ? updatedCard : card
+    );
+    gameState.draggingCard = updatedCard;
+
+    renderAllCards(gameState.cards);
+};
+
+const handleMouseUp = e => {
+    if (!gameState.draggingCard) {
+        const rect = canvas.getBoundingClientRect();
+        const x = e.clientX - rect.left;
+        const y = e.clientY - rect.top;
+
+        // Was the card clicked?
+        const clickedCard = gameState.cards.slice().reverse().find(card => isPointInCard(x, y, card));
+        if (clickedCard) {
+            // Move the clicked card to the top of the stack
+            gameState.cards = gameState.cards.filter(card => card !== clickedCard);
+            gameState.cards.push(clickedCard);
+            renderAllCards(gameState.cards); // Re-render all cards
+        }
+    }
+
+    gameState.draggingCard = null;
+    document.removeEventListener('mousemove', handleMouseMove);
+    document.removeEventListener('mouseup', handleMouseUp);
+};
+
+let dragOffset = { x: 0, y: 0 }; // To store the offset of the click position
+
+/**
+ * Finds the card that was clicked.
+ * @param {number} x - The x-coordinate of the click.
+ * @param {number} y - The y-coordinate of the click.
+ * @param {Card[]} cards - The list of cards to search through by card object reference.
+ * @returns {Card|null} The card that was clicked, or null if no card was clicked.
+ */
+const findClickedCard = (x, y, cards) => 
+    cards.slice().reverse().find(card => isPointInCard(x, y, card));
+
+/**
+ * Moves a card to the top of the stack.
+ * @param {Card} targetCard - The card to move to the top.
+ * @param {Card[]} cards - The list of cards to search through by card object reference.
+ * @returns {Card[]} A new array with the target card moved to the top.
+ */
+const moveCardToTop = (targetCard, cards) => [
+    ...cards.filter(card => card !== targetCard),
+    targetCard
+];
+
+const handleMouseDown = e => {
+    const rect = canvas.getBoundingClientRect();
+    const x = e.clientX - rect.left;
+    const y = e.clientY - rect.top;
+
+    if (e.button === 2) {
+        e.preventDefault();
+        const clickedCard = findClickedCard(x, y, gameState.cards);
+        if (clickedCard) {
+            const updatedCard = toggleCardFace(clickedCard);
+            gameState.cards = gameState.cards.map(card => 
+                card === clickedCard ? updatedCard : card
+            );
+            renderAllCards(gameState.cards);
+        }
+        return;
+    }
+
+    const clickedCard = findClickedCard(x, y, gameState.cards);
+    if (clickedCard) {
+        gameState.draggingCard = clickedCard;
+        dragOffset = {
+            x: x - clickedCard.x,
+            y: y - clickedCard.y
+        };
+        gameState.cards = moveCardToTop(clickedCard, gameState.cards);
+        
+        document.addEventListener('mousemove', handleMouseMove);
+        document.addEventListener('mouseup', handleMouseUp);
+    }
+};
+
+const handleResetGame = () => {
+    if (confirm("Would you like to reset the cards?")) {
+        resetCardsToOriginalPiles();
+    }
+};
+
+/**
+ * Moves a card to a new position.
+ * @param {Card} card - The card to move.
+ * @param {number} newX - The new x-coordinate for the card.
+ * @param {number} newY - The new y-coordinate for the card.
+ * @returns {Card} A new card object with updated position.
+ */
+const moveCard = (card, newX, newY) => ({
+    ...card,
+    x: newX,
+    y: newY
+});
+
+const toggleCardFace = card => ({
+    ...card,
+    isFaceUp: !card.isFaceUp
+});
+
+/**
+ * Calculates some stats for each deck, including total and face-up counts.
+ * @returns {Map} A map containing the total and face-up counts for each deck. 
+ * Useful for debugging information to the canvas.
+ */
+const getDeckStats = () => {
+    const stats = new Map();
+    gameState.cards.forEach(card => {
+        const deckId = card.card.deckId;
+        const current = stats.get(deckId) || { total: 0, faceUp: 0 };
+        stats.set(deckId, {
+            total: current.total + 1,
+            faceUp: current.faceUp + (card.isFaceUp ? 1 : 0)
+        });
+    });
+    return stats;
+};
+
+const renderDeckStats = () => {
+    const stats = getDeckStats();
+    ctx.font = '16px "pokemon-font", monospace';
+    
+    // Calculate the same starting X position as the piles
+    const totalWidth = PILE_SPACING * DECK_COUNT;
+    const startX = (canvas.width - totalWidth) / 2;
+    
+    stats.forEach((stat, deckId) => {
+        const colors = BASE_COLORS[deckId % BASE_COLORS.length];
+        const pileX = startX + (deckId * PILE_SPACING);
+        
+        ctx.fillStyle = colors.primary;
+        ctx.textAlign = 'center';
+        ctx.fillText(
+            `Deck ${deckId + 1}: ${stat.faceUp}/${stat.total}`, 
+            pileX + CARD_WIDTH / 2,
+            INITIAL_CARD_Y - 10
+        );
+    });
+};
+
+// FIXME: this is too complicated, and would probably work better if I had a better way of handling state. 
+const resetCardsToOriginalPiles = () => {
+    const totalWidth = PILE_SPACING * DECK_COUNT;
+    const startX = (canvas.width - totalWidth) / 2;
+
+    // Group cards by deck
+    const cardsByDeck = gameState.cards.reduce((acc, card) => {
+        const deckId = card.card.deckId;
+        if (!acc[deckId]) acc[deckId] = [];
+        acc[deckId].push(card);
+        return acc;
+    }, {});
+
+    // Reset position for each deck
+    Object.entries(cardsByDeck).forEach(([deckId, deckCards]) => {
+        const pileX = startX + (parseInt(deckId) * PILE_SPACING);
+        
+        deckCards.forEach((card, index) => {
+            card.x = pileX;
+            card.y = INITIAL_CARD_Y + (index * PILE_OFFSET);
+            card.isFaceUp = false;
+        });
+    });
+
+    renderAllCards(gameState.cards);
+};
+
+initializeGame();
+
+window.addEventListener('unload', () => {
+    canvas.removeEventListener('mousedown', handleMouseDown);
+    canvas.removeEventListener('contextmenu', e => e.preventDefault());
+}); 
\ No newline at end of file
diff --git a/html/cards/fonts/DotGothic16-Regular.ttf b/html/cards/fonts/DotGothic16-Regular.ttf
new file mode 100644
index 0000000..6634bc1
--- /dev/null
+++ b/html/cards/fonts/DotGothic16-Regular.ttf
Binary files differdiff --git a/html/cards/fonts/OFL copy.txt b/html/cards/fonts/OFL copy.txt
new file mode 100644
index 0000000..7c6649c
--- /dev/null
+++ b/html/cards/fonts/OFL copy.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The DotGothic16 Project Authors (https://github.com/fontworks-fonts/DotGothic16)

+

+This Font Software is licensed under the SIL Open Font License, Version 1.1.

+This license is copied below, and is also available with a FAQ at:

+https://openfontlicense.org

+

+

+-----------------------------------------------------------

+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

+-----------------------------------------------------------

+

+PREAMBLE

+The goals of the Open Font License (OFL) are to stimulate worldwide

+development of collaborative font projects, to support the font creation

+efforts of academic and linguistic communities, and to provide a free and

+open framework in which fonts may be shared and improved in partnership

+with others.

+

+The OFL allows the licensed fonts to be used, studied, modified and

+redistributed freely as long as they are not sold by themselves. The

+fonts, including any derivative works, can be bundled, embedded, 

+redistributed and/or sold with any software provided that any reserved

+names are not used by derivative works. The fonts and derivatives,

+however, cannot be released under any other type of license. The

+requirement for fonts to remain under this license does not apply

+to any document created using the fonts or their derivatives.

+

+DEFINITIONS

+"Font Software" refers to the set of files released by the Copyright

+Holder(s) under this license and clearly marked as such. This may

+include source files, build scripts and documentation.

+

+"Reserved Font Name" refers to any names specified as such after the

+copyright statement(s).

+

+"Original Version" refers to the collection of Font Software components as

+distributed by the Copyright Holder(s).

+

+"Modified Version" refers to any derivative made by adding to, deleting,

+or substituting -- in part or in whole -- any of the components of the

+Original Version, by changing formats or by porting the Font Software to a

+new environment.

+

+"Author" refers to any designer, engineer, programmer, technical

+writer or other person who contributed to the Font Software.

+

+PERMISSION & CONDITIONS

+Permission is hereby granted, free of charge, to any person obtaining

+a copy of the Font Software, to use, study, copy, merge, embed, modify,

+redistribute, and sell modified and unmodified copies of the Font

+Software, subject to the following conditions:

+

+1) Neither the Font Software nor any of its individual components,

+in Original or Modified Versions, may be sold by itself.

+

+2) Original or Modified Versions of the Font Software may be bundled,

+redistributed and/or sold with any software, provided that each copy

+contains the above copyright notice and this license. These can be

+included either as stand-alone text files, human-readable headers or

+in the appropriate machine-readable metadata fields within text or

+binary files as long as those fields can be easily viewed by the user.

+

+3) No Modified Version of the Font Software may use the Reserved Font

+Name(s) unless explicit written permission is granted by the corresponding

+Copyright Holder. This restriction only applies to the primary font name as

+presented to the users.

+

+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

+Software shall not be used to promote, endorse or advertise any

+Modified Version, except to acknowledge the contribution(s) of the

+Copyright Holder(s) and the Author(s) or with their explicit written

+permission.

+

+5) The Font Software, modified or unmodified, in part or in whole,

+must be distributed entirely under this license, and must not be

+distributed under any other license. The requirement for fonts to

+remain under this license does not apply to any document created

+using the Font Software.

+

+TERMINATION

+This license becomes null and void if any of the above conditions are

+not met.

+

+DISCLAIMER

+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

+OTHER DEALINGS IN THE FONT SOFTWARE.

diff --git a/html/cards/fonts/OFL.txt b/html/cards/fonts/OFL.txt
new file mode 100644
index 0000000..70041e1
--- /dev/null
+++ b/html/cards/fonts/OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2012 The Press Start 2P Project Authors (cody@zone38.net), with Reserved Font Name "Press Start 2P".

+

+This Font Software is licensed under the SIL Open Font License, Version 1.1.

+This license is copied below, and is also available with a FAQ at:

+https://openfontlicense.org

+

+

+-----------------------------------------------------------

+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007

+-----------------------------------------------------------

+

+PREAMBLE

+The goals of the Open Font License (OFL) are to stimulate worldwide

+development of collaborative font projects, to support the font creation

+efforts of academic and linguistic communities, and to provide a free and

+open framework in which fonts may be shared and improved in partnership

+with others.

+

+The OFL allows the licensed fonts to be used, studied, modified and

+redistributed freely as long as they are not sold by themselves. The

+fonts, including any derivative works, can be bundled, embedded, 

+redistributed and/or sold with any software provided that any reserved

+names are not used by derivative works. The fonts and derivatives,

+however, cannot be released under any other type of license. The

+requirement for fonts to remain under this license does not apply

+to any document created using the fonts or their derivatives.

+

+DEFINITIONS

+"Font Software" refers to the set of files released by the Copyright

+Holder(s) under this license and clearly marked as such. This may

+include source files, build scripts and documentation.

+

+"Reserved Font Name" refers to any names specified as such after the

+copyright statement(s).

+

+"Original Version" refers to the collection of Font Software components as

+distributed by the Copyright Holder(s).

+

+"Modified Version" refers to any derivative made by adding to, deleting,

+or substituting -- in part or in whole -- any of the components of the

+Original Version, by changing formats or by porting the Font Software to a

+new environment.

+

+"Author" refers to any designer, engineer, programmer, technical

+writer or other person who contributed to the Font Software.

+

+PERMISSION & CONDITIONS

+Permission is hereby granted, free of charge, to any person obtaining

+a copy of the Font Software, to use, study, copy, merge, embed, modify,

+redistribute, and sell modified and unmodified copies of the Font

+Software, subject to the following conditions:

+

+1) Neither the Font Software nor any of its individual components,

+in Original or Modified Versions, may be sold by itself.

+

+2) Original or Modified Versions of the Font Software may be bundled,

+redistributed and/or sold with any software, provided that each copy

+contains the above copyright notice and this license. These can be

+included either as stand-alone text files, human-readable headers or

+in the appropriate machine-readable metadata fields within text or

+binary files as long as those fields can be easily viewed by the user.

+

+3) No Modified Version of the Font Software may use the Reserved Font

+Name(s) unless explicit written permission is granted by the corresponding

+Copyright Holder. This restriction only applies to the primary font name as

+presented to the users.

+

+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font

+Software shall not be used to promote, endorse or advertise any

+Modified Version, except to acknowledge the contribution(s) of the

+Copyright Holder(s) and the Author(s) or with their explicit written

+permission.

+

+5) The Font Software, modified or unmodified, in part or in whole,

+must be distributed entirely under this license, and must not be

+distributed under any other license. The requirement for fonts to

+remain under this license does not apply to any document created

+using the Font Software.

+

+TERMINATION

+This license becomes null and void if any of the above conditions are

+not met.

+

+DISCLAIMER

+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,

+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF

+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT

+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE

+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,

+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL

+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING

+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM

+OTHER DEALINGS IN THE FONT SOFTWARE.

diff --git a/html/cards/fonts/PressStart2P-Regular.ttf b/html/cards/fonts/PressStart2P-Regular.ttf
new file mode 100644
index 0000000..2442aff
--- /dev/null
+++ b/html/cards/fonts/PressStart2P-Regular.ttf
Binary files differdiff --git a/html/cards/index.html b/html/cards/index.html
new file mode 100644
index 0000000..6c6c25e
--- /dev/null
+++ b/html/cards/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="One should always play fairly when one has the winning cards - Oscar Wilde">
+    <title>πŸƒ cards πŸƒ</title>
+    <style>
+        @font-face {
+            font-family: 'pokemon-font';
+            src: url('./pokemon-font/fonts/pokemon-font.ttf') format('ttf'); /* IE9 Compat Modes */
+            src: url('./pokemon-font/fonts/pokemon-font.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
+                url('./pokemon-font/fonts/pokemon-font.woff2') format('woff2'), /* Super Modern Browsers */
+                url('./pokemon-font/fonts/pokemon-font.woff') format('woff'), /* Pretty Modern Browsers */
+                url('./pokemon-font/fonts/pokemon-font.ttf')  format('truetype') /* Safari, Android, iOS */
+        }
+
+        body {
+            margin: 0;
+            padding: 0;
+            font-smooth: never;
+            -webkit-font-smoothing: none;
+            font-family: "pokemon-font", monospace;
+            font-size: 16px;
+        }
+
+        canvas {
+            display: block;
+        }
+    </style>
+</head>
+<body>
+    <canvas id="cards"></canvas>
+    <script src="./cards.js"></script>
+</body>
+</html>
diff --git a/html/cards/pokemon-font b/html/cards/pokemon-font
new file mode 160000
+Subproject 81b60805150c75ebfdfcec6d8352c67e491f8a6
diff --git a/html/voxels/index.html b/html/isometric-bounce/index.html
index 570f247..570f247 100644
--- a/html/voxels/index.html
+++ b/html/isometric-bounce/index.html
diff --git a/html/voxels/js/game.js b/html/isometric-bounce/js/game.js
index fb6530d..a1849c8 100644
--- a/html/voxels/js/game.js
+++ b/html/isometric-bounce/js/game.js
@@ -8,6 +8,7 @@ function createGame() {
         offsetX: 0,
         offsetY: 0,
         particles: [],
+        lastFrameTime: 0,
         player: {
             x: 0,
             y: 0,
@@ -21,7 +22,9 @@ function createGame() {
             isJumping: false,
             startX: 0,
             startY: 0
-        }
+        },
+        isHopping: false,
+        hopProgress: 0
     };
 
     state.ctx = state.canvas.getContext('2d');
@@ -122,30 +125,42 @@ function createGame() {
         const jumpDuration = 0.1;
         const maxJumpHeight = state.tileHeight;
 
-        if (!state.player.currentWaypoint && state.player.path.length > 0) {
-            state.player.currentWaypoint = state.player.path.shift();
-            state.player.isJumping = true;
-            state.player.jumpProgress = 0;
-            state.player.startX = state.player.x;
-            state.player.startY = state.player.y;
-        }
-
-        if (state.player.currentWaypoint && state.player.isJumping) {
-            state.player.jumpProgress += jumpDuration;
-            state.player.jumpProgress = Math.min(state.player.jumpProgress, 1);
-            
-            state.player.jumpHeight = Math.sin(state.player.jumpProgress * Math.PI) * maxJumpHeight;
-            
-            state.player.x = state.player.startX + (state.player.currentWaypoint.x - state.player.startX) * state.player.jumpProgress;
-            state.player.y = state.player.startY + (state.player.currentWaypoint.y - state.player.startY) * state.player.jumpProgress;
+        if (state.isHopping) {
+            state.hopProgress += jumpDuration;
+            state.hopProgress = Math.min(state.hopProgress, 1);
             
-            if (state.player.jumpProgress >= 1) {
-                state.player.isJumping = false;
+            state.player.jumpHeight = Math.sin(state.hopProgress * Math.PI) * maxJumpHeight;
+
+            if (state.hopProgress >= 1) {
+                state.isHopping = false;
                 state.player.jumpHeight = 0;
-                state.player.x = state.player.currentWaypoint.x;
-                state.player.y = state.player.currentWaypoint.y;
-                dustyParticles(state.player.x, state.player.y);
-                state.player.currentWaypoint = null;
+            }
+        } else {
+            if (!state.player.currentWaypoint && state.player.path.length > 0) {
+                state.player.currentWaypoint = state.player.path.shift();
+                state.player.isJumping = true;
+                state.player.jumpProgress = 0;
+                state.player.startX = state.player.x;
+                state.player.startY = state.player.y;
+            }
+
+            if (state.player.currentWaypoint && state.player.isJumping) {
+                state.player.jumpProgress += jumpDuration;
+                state.player.jumpProgress = Math.min(state.player.jumpProgress, 1);
+                
+                state.player.jumpHeight = Math.sin(state.player.jumpProgress * Math.PI) * maxJumpHeight;
+                
+                state.player.x = state.player.startX + (state.player.currentWaypoint.x - state.player.startX) * state.player.jumpProgress;
+                state.player.y = state.player.startY + (state.player.currentWaypoint.y - state.player.startY) * state.player.jumpProgress;
+                
+                if (state.player.jumpProgress >= 1) {
+                    state.player.isJumping = false;
+                    state.player.jumpHeight = 0;
+                    state.player.x = state.player.currentWaypoint.x;
+                    state.player.y = state.player.currentWaypoint.y;
+                    dustyParticles(state.player.x, state.player.y);
+                    state.player.currentWaypoint = null;
+                }
             }
         }
     }
@@ -199,7 +214,7 @@ function createGame() {
 
     function drawPlayer() {
         const iso = toIsometric(state.player.x, state.player.y);
-        const jumpOffset = state.player.jumpHeight || 0;
+        const jumpOffset = state.player.jumpHeight || state.player.jumpHeight;
         
         let squashStretch = 1;
         if (state.player.isJumping) {
@@ -251,14 +266,21 @@ function createGame() {
         state.ctx.stroke();
     }
 
-    function gameLoop() {
-        state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
+    function gameLoop(timestamp) {
+
+        const frameInterval = 1000 / 60;
         
-        drawGrid();
-        updateParticles();
-        drawParticles();
-        updatePlayer();
-        drawPlayer();
+        if (!state.lastFrameTime || timestamp - state.lastFrameTime >= frameInterval) {
+            state.ctx.clearRect(0, 0, state.canvas.width, state.canvas.height);
+            
+            drawGrid();
+            updateParticles();
+            drawParticles();
+            updatePlayer();
+            drawPlayer();
+            
+            state.lastFrameTime = timestamp;
+        }
         
         requestAnimationFrame(gameLoop);
     }
@@ -270,9 +292,18 @@ function createGame() {
         
         const gridPos = fromIsometric(clickX, clickY);
         
-        if (gridPos.x >= 0 && gridPos.x < state.gridSize &&
-            gridPos.y >= 0 && gridPos.y < state.gridSize) {
-            
+        const iso = toIsometric(state.player.x, state.player.y);
+        const playerRadius = state.player.size;
+        const distanceToPlayer = Math.sqrt(
+            Math.pow(clickX - (iso.x + state.offsetX), 2) +
+            Math.pow(clickY - (iso.y + state.offsetY), 2)
+        );
+
+        if (distanceToPlayer < playerRadius) {
+            state.isHopping = true;
+            state.hopProgress = 0;
+        } else if (gridPos.x >= 0 && gridPos.x < state.gridSize &&
+                   gridPos.y >= 0 && gridPos.y < state.gridSize) {
             state.player.targetX = Math.round(gridPos.x);
             state.player.targetY = Math.round(gridPos.y);
             
@@ -291,6 +322,7 @@ function createGame() {
         resizeCanvas();
         window.addEventListener('resize', resizeCanvas);
         state.canvas.addEventListener('click', handleClick);
+        state.lastFrameTime = 0;
         gameLoop();
     }
 
diff --git a/html/matt-chat/ChicagoFLF.ttf b/html/matt-chat/ChicagoFLF.ttf
new file mode 100644
index 0000000..60691e1
--- /dev/null
+++ b/html/matt-chat/ChicagoFLF.ttf
Binary files differdiff --git a/html/matt-chat/cat.png b/html/matt-chat/cat.png
new file mode 100644
index 0000000..7d4c0b9
--- /dev/null
+++ b/html/matt-chat/cat.png
Binary files differdiff --git a/html/matt-chat/com.user.server.plist b/html/matt-chat/com.user.server.plist
new file mode 100644
index 0000000..b5fb9dd
--- /dev/null
+++ b/html/matt-chat/com.user.server.plist
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>Label</key>
+    <string>com.user.server</string>
+    <key>ProgramArguments</key>
+    <array>
+        <string>/Users/eli/Code/institute/tour/html/matt-chat/server.sh</string>
+    </array>
+    <key>RunAtLoad</key>
+    <true/>
+    <key>KeepAlive</key>
+    <true/>
+</dict>
+</plist>
\ No newline at end of file
diff --git a/html/matt-chat/index.html b/html/matt-chat/index.html
new file mode 100644
index 0000000..2bc8119
--- /dev/null
+++ b/html/matt-chat/index.html
@@ -0,0 +1,1266 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="Chatty chat chat chat. A super simple chat interface for the Ollama API.">
+    <title>matt chat is not a cat</title>
+    <meta name="theme-color" content="#007BFF">
+    <link rel="icon" href="cat.png" type="image/x-icon">
+    <link rel="shortcut icon" href="cat.png" type="image/x-icon">
+    <link rel="apple-touch-icon" href="cat.png">  
+    <link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            font-size: 22px;
+            margin: 0;
+            padding: 20px;
+            background-color: #f7f7f7;
+            max-width: 800px;
+            margin: 0 auto;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            height: 100vh;
+            overflow: hidden;
+        }
+        #chat-container {
+            background-color: white;
+            border: 1px solid #ccc;
+            border-radius: 8px;
+            padding: 1em;
+            margin: 0 auto;
+            flex: 1;
+            overflow-y: auto;
+            width: 100%;
+            max-height: 400px;
+            scroll-behavior: smooth;
+        }
+        #user-input {
+            width: 100%;
+            padding: 10px;
+            border-radius: 4px;
+            border: 1px solid #ddd;
+            font-size: 16px;
+            margin-top: 10px;
+            box-sizing: border-box;
+        }
+        #send-button {
+            padding: 10px 15px;
+            border-radius: 4px;
+            background-color: #007BFF;
+            color: white;
+            border: none;
+            cursor: pointer;
+            margin-top: 10px;
+            width: 100%;
+        }
+        #send-button:hover {
+            background-color: #0056b3;
+        }
+
+        .model-select-container {
+            align-self: flex-start;
+            width: 100%;
+            display: flex;
+            justify-content: space-between;
+            padding: 1em;
+        }
+
+        .model-select-container label {
+            margin-left: 10px;
+        }
+
+        .message {
+            white-space: pre-wrap;
+            margin-bottom: 10px;
+            padding: 1em;
+            border-radius: 8px;
+            background-color: #f1f1f1;
+            display: block;
+            max-width: 100%;
+        }
+
+        .user-message {
+            background-color: #007BFF;
+            color: white;
+            text-align: right;
+            margin-left: 20px;
+        }
+
+        .bot-message {
+            background-color: #f0f0f0;
+            color: #333;
+            text-align: left;
+            margin-right: 20px;
+        }
+
+        @media (max-width: 600px) {
+            #chat-container {
+                max-height: 300px;
+            }
+        }
+
+        body.dark-mode {
+            background-color: #333;
+            color: #f7f7f7;
+        }
+
+        #chat-container.dark-mode {
+            background-color: #444;
+            border: 1px solid #555;
+        }
+
+        #user-input.dark-mode {
+            background-color: #555;
+            color: #f7f7f7;
+            border: 1px solid #666;
+        }
+
+        #send-button.dark-mode {
+            background-color: #007BFF;
+            color: white;
+        }
+
+        .message.dark-mode {
+            background-color: #555;
+            color: #f7f7f7;
+        }
+
+        .user-message.dark-mode {
+            background-color: #007BFF;
+            color: white;
+        }
+
+        .bot-message.dark-mode {
+            background-color: #666;
+            color: #f7f7f7;
+        }
+
+        .bot-time {
+            margin: 0.5em 0;
+            font-size: 0.9em;
+            color: #888;
+            text-align: center;
+        }
+        
+        /* Professional theme */
+        body.theme-professional {
+            font-family: Arial, sans-serif;
+            font-size: 22px;
+        }
+
+        /* Molly Millions theme */
+        body.theme-molly-millions {
+            font-family: "Courier New", monospace;
+            font-size: 22px;
+            margin: 0;
+            padding: 20px;
+            background-color: #0a0a0a;
+            color: #00ff00;
+            max-width: 800px;
+            margin: 0 auto;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            height: 100vh;
+            overflow: hidden;
+        }
+
+        .theme-molly-millions #chat-container {
+            background-color: #000000;
+            border: 2px solid #00ff00;
+            border-radius: 0;
+            padding: 1em;
+            margin: 0 auto;
+            flex: 1;
+            overflow-y: auto;
+            width: 100%;
+            max-height: 400px;
+            scroll-behavior: smooth;
+            box-shadow: 0 0 10px #00ff00;
+        }
+
+        .theme-molly-millions #user-input {
+            width: 100%;
+            padding: 10px;
+            border-radius: 0;
+            border: 2px solid #00ff00;
+            background-color: #000000;
+            color: #00ff00;
+            font-family: "Courier New", monospace;
+            font-size: 16px;
+            margin-top: 10px;
+            box-sizing: border-box;
+        }
+
+        .theme-molly-millions #send-button {
+            padding: 10px 15px;
+            border-radius: 0;
+            background-color: #000000;
+            color: #00ff00;
+            border: 2px solid #00ff00;
+            cursor: pointer;
+            margin-top: 10px;
+            width: 100%;
+            font-family: "Courier New", monospace;
+            text-transform: uppercase;
+        }
+
+        .theme-molly-millions #send-button:hover {
+            background-color: #00ff00;
+            color: #000000;
+        }
+
+        .theme-molly-millions .message {
+            white-space: pre-wrap;
+            margin-bottom: 10px;
+            padding: 1em;
+            border-radius: 0;
+            border: 1px solid #00ff00;
+            background-color: #0a0a0a;
+            display: block;
+            max-width: 100%;
+        }
+
+        .theme-molly-millions .user-message {
+            background-color: #001100;
+            color: #00ff00;
+            border: 1px solid #00ff00;
+            text-align: right;
+            margin-left: 20px;
+        }
+
+        .theme-molly-millions .bot-message {
+            background-color: #000000;
+            color: #00ff00;
+            border: 1px solid #00ff00;
+            text-align: left;
+            margin-right: 20px;
+        }
+
+        .theme-molly-millions .bot-time {
+            color: #005500;
+        }
+
+        /* Cloud theme */
+        body.theme-cloud {
+            font-family: "Press Start 2P", "Courier New", monospace;
+            font-size: 18px;
+            margin: 0;
+            padding: 20px;
+            background: linear-gradient(135deg, #1a1b4b 0%, #162057 50%, #1a1b4b 100%);
+            color: #ffffff;
+            max-width: 800px;
+            margin: 0 auto;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            height: 100vh;
+            overflow: hidden;
+        }
+
+        .theme-cloud #chat-container {
+            background: rgba(0, 0, 32, 0.75);
+            border: 3px solid #4080ff;
+            border-radius: 3px;
+            padding: 1em;
+            margin: 0 auto;
+            flex: 1;
+            overflow-y: auto;
+            width: 100%;
+            max-height: 400px;
+            scroll-behavior: smooth;
+            box-shadow: 0 0 15px rgba(64, 128, 255, 0.3);
+        }
+
+        .theme-cloud #user-input {
+            width: 100%;
+            padding: 10px;
+            border: 2px solid #4080ff;
+            background: rgba(0, 0, 32, 0.75);
+            color: #ffffff;
+            font-family: "Press Start 2P", "Courier New", monospace;
+            font-size: 14px;
+            margin-top: 10px;
+            box-sizing: border-box;
+        }
+
+        .theme-cloud #send-button {
+            padding: 10px 15px;
+            background: linear-gradient(to bottom, #4080ff 0%, #2048c0 100%);
+            color: white;
+            border: 2px solid #2048c0;
+            cursor: pointer;
+            margin-top: 10px;
+            width: 100%;
+            font-family: "Press Start 2P", "Courier New", monospace;
+            font-size: 14px;
+            text-transform: uppercase;
+            text-shadow: 2px 2px #000000;
+        }
+
+        .theme-cloud #send-button:hover {
+            background: linear-gradient(to bottom, #50a0ff 0%, #3060e0 100%);
+        }
+
+        .theme-cloud .message {
+            white-space: pre-wrap;
+            margin-bottom: 10px;
+            padding: 1em;
+            border: 2px solid #4080ff;
+            background: rgba(0, 0, 32, 0.5);
+            display: block;
+            max-width: 100%;
+            font-size: 14px;
+        }
+
+        .theme-cloud .user-message {
+            background: rgba(64, 128, 255, 0.2);
+            color: #ffffff;
+            border: 2px solid #4080ff;
+            text-align: right;
+            margin-left: 20px;
+            text-shadow: 1px 1px #000000;
+        }
+
+        .theme-cloud .bot-message {
+            background: rgba(32, 64, 128, 0.2);
+            color: #ffffff;
+            border: 2px solid #4080ff;
+            text-align: left;
+            margin-right: 20px;
+            text-shadow: 1px 1px #000000;
+        }
+
+        .theme-cloud .bot-time {
+            color: #80c0ff;
+            font-size: 12px;
+            text-shadow: 1px 1px #000000;
+        }
+
+        .theme-cloud #counter {
+            color: #80c0ff !important;
+            text-shadow: 1px 1px #000000;
+        }
+
+        .theme-cloud .model-select-container {
+            background: rgba(0, 0, 32, 0.75);
+            border: 2px solid #4080ff;
+            padding: 10px;
+            margin-bottom: 10px;
+            width: 100%;
+            box-sizing: border-box;
+        }
+
+        .theme-cloud #model-select {
+            background: rgba(0, 0, 32, 0.75);
+            color: #ffffff;
+            border: 1px solid #4080ff;
+            padding: 5px;
+            font-family: "Press Start 2P", "Courier New", monospace;
+            font-size: 12px;
+        }
+
+        /* Classic Mac theme */
+        @font-face {
+            font-family: 'ChicagoFLF';
+            src: url('/ChicagoFLF.ttf') format('truetype');
+        }
+
+        body.theme-classic {
+            font-family: 'ChicagoFLF', 'Monaco', monospace;
+            font-size: 14px;
+            margin: 0;
+            padding: 20px;
+            background-color: #DDDDDD;
+            color: #000000;
+            max-width: 800px;
+            margin: 0 auto;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            height: 100vh;
+            overflow: hidden;
+            image-rendering: pixelated;
+        }
+
+        .theme-classic #chat-container {
+            background-color: #FFFFFF;
+            border: 2px solid #000000;
+            border-radius: 2px;
+            padding: 1em;
+            margin: 0 auto;
+            flex: 1;
+            overflow-y: auto;
+            width: 100%;
+            max-height: 400px;
+            scroll-behavior: smooth;
+            box-shadow: 2px 2px 0px #000000;
+        }
+
+        .theme-classic #user-input {
+            width: 100%;
+            padding: 8px;
+            border: 2px solid #000000;
+            background-color: #FFFFFF;
+            color: #000000;
+            font-family: 'ChicagoFLF', 'Monaco', monospace;
+            font-size: 14px;
+            margin-top: 10px;
+            box-sizing: border-box;
+            border-radius: 2px;
+        }
+
+        .theme-classic #send-button {
+            padding: 4px 15px;
+            background-color: #FFFFFF;
+            color: #000000;
+            border: 2px solid #000000;
+            border-radius: 2px;
+            cursor: pointer;
+            margin-top: 10px;
+            width: 100%;
+            font-family: 'ChicagoFLF', 'Monaco', monospace;
+            font-size: 14px;
+            box-shadow: 2px 2px 0px #000000;
+        }
+
+        .theme-classic #send-button:hover {
+            background-color: #000000;
+            color: #FFFFFF;
+        }
+
+        .theme-classic #send-button:active {
+            box-shadow: 1px 1px 0px #000000;
+            transform: translate(1px, 1px);
+        }
+
+        .theme-classic .message {
+            white-space: pre-wrap;
+            margin-bottom: 10px;
+            padding: 8px;
+            border: 2px solid #000000;
+            background-color: #FFFFFF;
+            display: block;
+            max-width: 100%;
+            font-size: 14px;
+            border-radius: 2px;
+        }
+
+        .theme-classic .user-message {
+            background-color: #FFFFFF;
+            color: #000000;
+            text-align: right;
+            margin-left: 20px;
+            box-shadow: 2px 2px 0px #000000;
+        }
+
+        .theme-classic .bot-message {
+            background-color: #FFFFFF;
+            color: #000000;
+            text-align: left;
+            margin-right: 20px;
+            box-shadow: 2px 2px 0px #000000;
+        }
+
+        .theme-classic .bot-time {
+            color: #666666;
+            font-size: 12px;
+            text-align: center;
+            margin: 4px 0;
+        }
+
+        .theme-classic #counter {
+            color: #000000 !important;
+        }
+
+        .theme-classic .model-select-container {
+            background-color: #FFFFFF;
+            border: 2px solid #000000;
+            padding: 8px;
+            margin-bottom: 10px;
+            width: 100%;
+            box-sizing: border-box;
+            border-radius: 2px;
+            box-shadow: 2px 2px 0px #000000;
+        }
+
+        .theme-classic #model-select {
+            background-color: #FFFFFF;
+            color: #000000;
+            border: 2px solid #000000;
+            padding: 2px;
+            font-family: 'ChicagoFLF', 'Monaco', monospace;
+            font-size: 14px;
+            border-radius: 2px;
+        }
+
+        .theme-classic input[type="checkbox"] {
+            appearance: none;
+            -webkit-appearance: none;
+            width: 16px;
+            height: 16px;
+            border: 2px solid #000000;
+            background-color: #FFFFFF;
+            position: relative;
+            vertical-align: middle;
+            margin-right: 5px;
+        }
+
+        .theme-classic input[type="checkbox"]:checked::after {
+            content: 'βœ“';
+            position: absolute;
+            left: 1px;
+            top: -2px;
+            font-size: 14px;
+        }
+
+        /* LCARS Theme */
+        body.theme-lcars {
+            font-family: "Helvetica Neue", Arial, sans-serif;
+            font-size: 18px;
+            margin: 0;
+            padding: 20px;
+            background-color: #000;
+            color: #FF9966;
+            max-width: 800px;
+            margin: 0 auto;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            height: 100vh;
+            overflow: hidden;
+        }
+
+        .theme-lcars #chat-container {
+            background-color: #000;
+            border: none;
+            border-radius: 0;
+            padding: 1em;
+            margin: 0 auto;
+            flex: 1;
+            overflow-y: auto;
+            width: 100%;
+            max-height: 400px;
+            scroll-behavior: smooth;
+            position: relative;
+        }
+
+        .theme-lcars #chat-container::before {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            height: 2em;
+            background: #CC6699;
+            border-radius: 20px 20px 0 0;
+        }
+
+        .theme-lcars #user-input {
+            width: 100%;
+            padding: 10px;
+            border: none;
+            background-color: #000;
+            color: #FF9966;
+            font-family: "Helvetica Neue", Arial, sans-serif;
+            font-size: 16px;
+            margin-top: 10px;
+            box-sizing: border-box;
+            border-left: 2em solid #CC6699;
+        }
+
+        .theme-lcars #send-button {
+            padding: 10px 15px;
+            background-color: #CC6699;
+            color: #000;
+            border: none;
+            cursor: pointer;
+            margin-top: 10px;
+            width: 100%;
+            font-family: "Helvetica Neue", Arial, sans-serif;
+            font-weight: bold;
+            font-size: 16px;
+            text-transform: uppercase;
+            border-radius: 0 0 20px 20px;
+        }
+
+        .theme-lcars #send-button:hover {
+            background-color: #FF9966;
+        }
+
+        .theme-lcars .message {
+            white-space: pre-wrap;
+            margin-bottom: 10px;
+            padding: 1em;
+            border: none;
+            display: block;
+            max-width: 100%;
+            position: relative;
+        }
+
+        .theme-lcars .user-message {
+            background-color: #000;
+            color: #FF9966;
+            text-align: right;
+            margin-left: 20px;
+            border-right: 1em solid #CC6699;
+        }
+
+        .theme-lcars .bot-message {
+            background-color: #000;
+            color: #99CCFF;
+            text-align: left;
+            margin-right: 20px;
+            border-left: 1em solid #9999CC;
+        }
+
+        .theme-lcars .bot-time {
+            color: #CC6699;
+            font-size: 0.8em;
+            text-align: center;
+            margin: 4px 0;
+        }
+
+        .theme-lcars #counter {
+            color: #99CCFF !important;
+        }
+
+        .theme-lcars .model-select-container {
+            background-color: #000;
+            border: none;
+            padding: 10px;
+            margin-bottom: 10px;
+            width: 100%;
+            box-sizing: border-box;
+            display: flex;
+            align-items: center;
+            border-radius: 20px;
+            position: relative;
+            overflow: hidden;
+        }
+
+        .theme-lcars .model-select-container::before {
+            content: '';
+            position: absolute;
+            top: 0;
+            left: 0;
+            bottom: 0;
+            width: 2em;
+            background: #9999CC;
+            border-radius: 20px 0 0 20px;
+        }
+
+        .theme-lcars #model-select {
+            background-color: #000;
+            color: #FF9966;
+            border: none;
+            padding: 5px;
+            margin-left: 3em;
+            font-family: "Helvetica Neue", Arial, sans-serif;
+            font-size: 16px;
+        }
+
+        .theme-lcars input[type="checkbox"] {
+            appearance: none;
+            -webkit-appearance: none;
+            width: 16px;
+            height: 16px;
+            border: 2px solid #CC6699;
+            background-color: #000;
+            position: relative;
+            vertical-align: middle;
+            margin-right: 5px;
+        }
+
+        .theme-lcars input[type="checkbox"]:checked {
+            background-color: #CC6699;
+        }
+
+        .theme-lcars input[type="checkbox"]:checked::after {
+            content: 'βœ“';
+            position: absolute;
+            left: 2px;
+            top: -2px;
+            color: #000;
+            font-size: 14px;
+        }
+    </style>
+</head>
+<body>
+
+    <div class="model-select-container">
+        <select id="model-select"></select>
+        <label>
+            <input type="checkbox" id="retain-history" /> Build Context As You Chat?
+        </label>
+    </div>
+    
+    <div id="chat-container">
+        <!-- Messages will appear here -->
+    </div>
+
+    <!-- New container for user input and send button -->
+    <div id="input-container" style="width: 100%; display: flex; flex-direction: column; margin-top: 10px;">
+        <div id="counter" style="text-align: left; font-size: 0.9em; color: #555;">
+            Characters: <span id="char-count">0</span> | Words: <span id="word-count">0</span>
+        </div>
+        <textarea id="user-input" placeholder="Type your message..."></textarea>
+        <button id="send-button">Send</button>
+    </div>
+
+    <script>
+        // ==================================================
+        // MATT CHAT IS NOT A CAT
+        // This is a simple chat interface for the Ollama API
+        // ==================================================
+        //
+        // This configuration object is used to define all local variables for your needs
+        // Set the base url for the ollama api, and then list all the models you want to use
+        // The context window size is the number of previous exchanges to keep...
+        // though this is relatively naive at the moment
+
+        const config = {}
+
+        const localConfig = {
+            apiUrl: "http://localhost:11434/v1",
+            completionsEndpoint: "http://localhost:11434/v1/chat/completions",
+            modelsEndpoint: "http://localhost:11434/v1/models",
+            contextWindowSize: 6,
+            systemMessage: "You are a helpful assistant. If you don't know something you'll let me know. Your name is Matt.",
+            maxTokens: 4096,
+            summarizeThreshold: 3584,
+        };
+
+        const mattConfig = {
+            apiUrl: "http://100.108.91.106:11434/v1",
+            completionsEndpoint: "http://100.108.91.106:11434/v1/chat/completions",
+            modelsEndpoint: "http://100.108.91.106:11434/v1/models",
+            contextWindowSize: 6,
+            systemMessage: "You are a helpful assistant. If you don't know something you'll let me know. Your name is Matt.",
+            maxTokens: 4096,
+            summarizeThreshold: 3584,
+        }
+
+        let conversationHistory = {
+            summary: null,
+            current: [],
+            full: []
+        };
+
+        let isCatMode = false; // Flag to track cat mode
+
+        const API_MODELS_ENDPOINT = config.modelsEndpoint;
+
+        // Add this near the top with other constants
+        const AVAILABLE_THEMES = {
+            'professional': 'Professional -- boring, like wearing a tie',
+            'molly-millions': 'Molly Millions\' manicure',
+            'cloud': 'Cloud -- it took a lot of self control not to add sound effects',
+            'classic': 'Classic -- this is not a fish',
+            'lcars': 'LCARS -- boldly going'
+        };
+
+        function handleError(message) {
+            console.error(message);
+            addMessage(message, "bot");
+        }
+
+        function showLoadingMessage() {
+            return addMessage("Loading models...", "bot");
+        }
+
+        async function populateModelSelect() {
+            const modelSelect = document.getElementById("model-select");
+            modelSelect.innerHTML = ""; // Clear existing options
+
+            const loadingMessage = showLoadingMessage();
+            const modelIds = [];
+
+            try {
+                const response = await fetch(config.modelsEndpoint);
+                if (!response.ok) throw new Error('Failed to fetch models');
+
+                const data = await response.json();
+                console.log("API Response:", data);
+
+                if (Array.isArray(data.data)) {
+                    data.data.forEach(model => {
+                        const option = document.createElement("option");
+                        option.value = model.id;
+                        option.textContent = model.id;
+                        modelSelect.appendChild(option);
+                        modelIds.push(model.id);
+                    });
+                    console.log("Model IDs:", modelIds);
+                } else {
+                    handleError("Expected an array of models, but got: " + JSON.stringify(data));
+                }
+            } catch (error) {
+                handleError("Error fetching models: " + error.message);
+            } finally {
+                loadingMessage.remove();
+                if (modelIds.length > 0) {
+                    addMessage(`Models loaded successfully! Ready to chat.\n\nAvailable models: ${modelIds.join(', ')}`, "bot");
+                } else {
+                    addMessage("No models available to chat.", "bot");
+                }
+            }
+        }
+
+        document.addEventListener("DOMContentLoaded", () => {
+            populateModelSelect();
+            const modelSelect = document.getElementById("model-select");
+            const savedModel = localStorage.getItem("selectedModel");
+            if (savedModel) {
+                modelSelect.value = savedModel;
+            }
+            modelSelect.addEventListener("change", () => {
+                localStorage.setItem("selectedModel", modelSelect.value);
+            });
+            const savedTheme = localStorage.getItem('selectedTheme') || 'professional';
+            switchTheme(savedTheme);
+        });
+
+        function addMessage(message, sender = "user") {
+            const chatContainer = document.getElementById("chat-container");
+            const messageElement = document.createElement("div");
+            messageElement.classList.add("message", sender === "user" ? "user-message" : "bot-message");
+            messageElement.textContent = message;
+            chatContainer.appendChild(messageElement);
+            messageElement.scrollIntoView({ behavior: "smooth", block: "end" });
+            chatContainer.scrollTop = chatContainer.scrollHeight; // Make sure the chat is scrolled to the bottom
+            return messageElement; // Return the message element so it is easier to use
+        }
+
+        // Fancy format milliseconds into a more readable format
+        function formatDuration(duration) {
+            const minutes = Math.floor(duration / (1000 * 60));
+            const seconds = Math.floor((duration % (1000 * 60)) / 1000);
+            const milliseconds = duration % 1000;
+            
+            if (minutes > 0) {
+                return `${minutes}m ${seconds}.${Math.floor(milliseconds / 10)}s`;
+            }
+            return `${seconds}.${Math.floor(milliseconds / 10)}s`;
+        }
+
+        // Character and word counter
+        function updateCounter() {
+            const userInput = document.getElementById("user-input");
+            const charCount = document.getElementById("char-count");
+            const wordCount = document.getElementById("word-count");
+
+            const text = userInput.value;
+            const characters = text.length;
+            const words = text.trim() ? text.trim().split(/\s+/).length : 0; // Count words
+
+            charCount.textContent = characters;
+            wordCount.textContent = words;
+        }
+
+        // Event listener to update the counter on input
+        document.getElementById("user-input").addEventListener("input", updateCounter);
+
+        function toggleCatMode() {
+            isCatMode = !isCatMode; // Toggle the flag
+            if (isCatMode) {
+                config.systemMessage += " You are a cat."; // Append the phrase
+            } else {
+                config.systemMessage = config.systemMessage.replace(" You are a large, fluffy cat. You are a little aloof, but kind.", ""); // Remove the phrase
+            }
+            addMessage(`Cat mode is now ${isCatMode ? "enabled" : "disabled"}.`, "bot"); // Inform the user
+        }
+
+        async function sendMessage() {
+            const userInput = document.getElementById("user-input");
+            const userMessage = userInput.value.trim();
+
+            if (!userMessage) return;
+
+            // Check for slash commands
+            if (userMessage.toLowerCase() === '/dark' || userMessage.toLowerCase() === '/darkmode') {
+                toggleDarkMode();
+                userInput.value = ""; // Clear input after command
+                updateCounter(); // Reset counters
+                return;
+            }
+
+            if (userMessage.toLowerCase() === '/clear') {
+                clearChat();
+                userInput.value = ""; // Clear input after command
+                updateCounter(); // Reset counters
+                return;
+            }
+
+            if (userMessage.toLowerCase() === '/help') {
+                displayHelp();
+                userInput.value = ""; // Clear input after command
+                updateCounter(); // Reset counters
+                return;
+            }
+
+            if (userMessage.toLowerCase() === '/cat' || userMessage.toLowerCase() === '/catmode') {
+                toggleCatMode(); // Toggle cat mode
+                userInput.value = ""; // Clear input after command
+                updateCounter(); // Reset counters
+                return;
+            }
+
+            if (userMessage.toLowerCase() === '/context') {
+                const context = viewCurrentContext();
+                addMessage(`Current conversation has ${context.currentMessages} messages\nEstimated tokens: ${context.estimatedTokens}`, "bot");
+                return;
+            }
+
+            if (userMessage.toLowerCase().startsWith('/theme')) {
+                const requestedTheme = userMessage.toLowerCase().split(' ')[1];
+                if (!requestedTheme) {
+                    // If no theme is specified, lets show all available themes
+                    addMessage(`Available themes: ${Object.keys(AVAILABLE_THEMES).join(', ')}`, "bot");
+                } else if (AVAILABLE_THEMES[requestedTheme]) {
+                    switchTheme(requestedTheme);
+                } else {
+                    addMessage(`Unknown theme. Available themes: ${Object.keys(AVAILABLE_THEMES).join(', ')}`, "bot");
+                }
+                userInput.value = "";
+                updateCounter();
+                return;
+            }
+
+            if (userMessage.toLowerCase() === '/matt') {
+                Object.assign(config, mattConfig);
+                addMessage("Switched to Matt's config", "bot");
+                userInput.value = "";
+                updateCounter();
+                populateModelSelect(); // Refresh the model list for the new endpoint
+                return;
+            }
+
+            if (userMessage.toLowerCase() === '/local') {
+                Object.assign(config, localConfig);
+                addMessage("Switched to local config", "bot");
+                userInput.value = "";
+                updateCounter();
+                populateModelSelect(); // Refresh the model list for the new endpoint
+                return;
+            }
+
+            addMessage(userMessage, "user");
+            userInput.value = ""; // Clear input after sending the message
+            
+            // Reset the counter
+            document.getElementById("char-count").textContent = "0";
+            document.getElementById("word-count").textContent = "0";
+
+            // Create and add loading indicator
+            const loadingIndicator = document.createElement("div");
+            loadingIndicator.id = "loading-indicator";
+            loadingIndicator.classList.add("message", "bot-message");
+            loadingIndicator.textContent = "...";
+            document.getElementById("chat-container").appendChild(loadingIndicator);
+            scrollToBottom();
+
+            // Start animation for this specific indicator
+            const animationInterval = animateLoadingIndicator(loadingIndicator);
+
+            const startTime = Date.now(); // Capture the start time
+
+            try {
+                const modelSelect = document.getElementById("model-select");
+                const selectedModel = modelSelect.value;
+                const retainHistory = document.getElementById("retain-history").checked; // Check the checkbox state
+
+                // Prepare the messages for the API
+                const messagesToSend = await prepareMessages(userMessage);
+
+                const response = await fetch(config.completionsEndpoint, {
+                    method: "POST",
+                    headers: {
+                        "Content-Type": "application/json",
+                    },
+                    body: JSON.stringify({
+                        model: selectedModel,
+                        messages: messagesToSend,
+                    }),
+                });
+
+                if (!response.ok) {
+                    throw new Error('Error communicating with Ollama API');
+                }
+
+                const data = await response.json();
+                console.log("API Response:", data);
+
+                if (data.choices && data.choices.length > 0) {
+                    const botResponse = data.choices[0].message.content;
+                    
+                    // Clear loading indicator
+                    clearInterval(animationInterval);
+                    loadingIndicator.remove();
+                    
+                    // Add bot's response to chat and history
+                    addMessage(botResponse, "bot");
+                    conversationHistory.current.push({ role: "assistant", content: botResponse });
+
+                    // Calculate and display duration
+                    const duration = Date.now() - startTime;
+                    const timeTakenMessage = formatDuration(duration);
+                    const timeDisplay = document.createElement("div");
+                    timeDisplay.classList.add("bot-time");
+                    timeDisplay.textContent = `Response time: ${timeTakenMessage}`;
+                    document.getElementById("chat-container").appendChild(timeDisplay);
+                    scrollToBottom();
+
+                } else {
+                    console.error("No response from API");
+                    loadingIndicator.remove();
+                    addMessage("Sorry, I didn't get a response from the assistant.", "bot");
+                }
+
+                if (conversationHistory.current.length > 10) {
+                    conversationHistory.current.shift(); // Remove the oldest message
+                }
+
+            } catch (error) {
+                console.error("Error:", error);
+                clearInterval(animationInterval);
+                loadingIndicator.remove();
+                addMessage("Sorry, there was an error processing your request.", "bot");
+            }
+        }
+
+        function animateLoadingIndicator(indicator) {
+            let dots = 0;
+            return setInterval(() => {
+                dots = (dots + 1) % 6;
+                if (indicator && document.contains(indicator)) {
+                    indicator.textContent = '.'.repeat(dots || 1);
+                }
+            }, 500);
+        }
+
+        document.getElementById("send-button").addEventListener("click", sendMessage);
+
+        document.getElementById("user-input").addEventListener("keypress", function (e) {
+            if (e.key === "Enter") {
+                e.preventDefault(); // Prevent line break
+                sendMessage();
+            }
+        });
+
+        function toggleDarkMode() {
+            const body = document.body;
+            const chatContainer = document.getElementById("chat-container");
+            const userInput = document.getElementById("user-input");
+            const sendButton = document.getElementById("send-button");
+
+            body.classList.toggle("dark-mode");
+            chatContainer.classList.toggle("dark-mode");
+            userInput.classList.toggle("dark-mode");
+            sendButton.classList.toggle("dark-mode");
+
+            // Update message classes
+            const messages = document.querySelectorAll(".message");
+            messages.forEach(message => {
+                message.classList.toggle("dark-mode");
+            });
+
+            // Save preference to local storage
+            const isDarkMode = body.classList.contains("dark-mode");
+            localStorage.setItem("darkMode", isDarkMode);
+        }
+
+        // Load dark mode preference from local storage on page load
+        document.addEventListener("DOMContentLoaded", () => {
+            const darkModePreference = localStorage.getItem("darkMode");
+            if (darkModePreference === "true") {
+                toggleDarkMode(); // Activate dark mode if preference is set
+            }
+        });
+
+        function clearChat() {
+            const chatContainer = document.getElementById("chat-container");
+            chatContainer.innerHTML = "";
+            conversationHistory = {
+                summary: null,
+                current: [],
+                full: []
+            };
+        }
+
+        function displayHelp() {
+            const helpMessage = `
+Available commands:\n
+  /dark - Toggle dark mode when using the professional theme
+  /cat - Toggle cat mode
+  /context - Show the current conversation's context
+  /clear - Clear the chat history
+  /help - Show this message
+  /theme [theme-name] - Switch theme (available themes: ${Object.keys(AVAILABLE_THEMES).join(', ')})
+      without a theme name, this will show all available themes, too
+  /local - Switch to local Ollama instance
+  /matt - Switch to Matt's Ollama instance
+            `;
+            addMessage(helpMessage, "bot");
+        }
+
+        function estimateTokens(text) {
+            // Rough estimation: ~4 chars per token for English text
+            return Math.ceil(text.length / 4);
+        }
+
+        function getContextSize(messages) {
+            return messages.reduce((sum, msg) => sum + estimateTokens(msg.content), 0);
+        }
+
+        async function summarizeConversation(messages) {
+            try {
+                const modelSelect = document.getElementById("model-select");
+                const selectedModel = modelSelect.value;
+
+                const response = await fetch(config.completionsEndpoint, {
+                    method: "POST",
+                    headers: {
+                        "Content-Type": "application/json",
+                    },
+                    body: JSON.stringify({
+                        model: selectedModel,
+                        messages: messages,
+                    }),
+                });
+
+                const data = await response.json();
+                return data.choices[0].message.content;
+            } catch (error) {
+                console.error("Error summarizing conversation:", error);
+                return null;
+            }
+        }
+
+        async function prepareMessages(userMessage) {
+            const messages = [];
+            
+            // Always start with system message
+            messages.push({ role: "system", content: config.systemMessage });
+            
+            if (document.getElementById("retain-history").checked) {
+                // If we have a summary, add it more naturally
+                if (conversationHistory.summary) {
+                    messages.push({
+                        role: "system",
+                        content: `Previous discussion: ${conversationHistory.summary}`
+                    });
+                }
+                
+                // Add current conversation segment
+                messages.push(...conversationHistory.current);
+            }
+            
+            // Add the new message to history before we check for summarization
+            const newMessage = { role: "user", content: userMessage };
+            conversationHistory.current.push(newMessage);
+            messages.push(newMessage);
+            
+            // Do we need to summarize?
+            const totalTokens = getContextSize(messages);
+            if (totalTokens > config.summarizeThreshold) {
+                // Move current messages to full history, except for the newest message
+                conversationHistory.full.push(...conversationHistory.current.slice(0, -1));
+                
+                // Supposedly this is a more natural summarization prompt...
+                const summary = await summarizeConversation([
+                    {
+                        role: "system",
+                        content: "Summarize this conversation's key points and context that would be important for continuing the discussion naturally. Be concise but maintain essential details."
+                    },
+                    ...conversationHistory.full
+                ]);
+                
+                if (summary) {
+                    conversationHistory.summary = summary;
+                    // Keep only the most recent messages for immediate context
+                    conversationHistory.current = conversationHistory.current.slice(-4);
+                    
+                    // Rebuild messages array with new summary
+                    return [
+                        { role: "system", content: config.systemMessage },
+                        { role: "system", content: `Previous discussion: ${summary}` },
+                        ...conversationHistory.current
+                    ];
+                }
+            }
+            
+            return messages;
+        }
+
+        // Clean up old messages periodically
+        function pruneConversationHistory() {
+            if (conversationHistory.full.length > 100) {
+                // Keep only the last 100 messages in full history
+                conversationHistory.full = conversationHistory.full.slice(-100);
+            }
+        }
+
+        // Call this after successful responses
+        setInterval(pruneConversationHistory, 60000); // Clean up every minute
+
+        function viewCurrentContext() {
+            const context = {
+                summary: conversationHistory.summary,
+                currentMessages: conversationHistory.current.length,
+                fullHistoryMessages: conversationHistory.full.length,
+                estimatedTokens: getContextSize(conversationHistory.current)
+            };
+            console.log("Current Context:", context);
+            return context;
+        }
+
+        function scrollToBottom() {
+            const chatContainer = document.getElementById("chat-container");
+            chatContainer.scrollTop = chatContainer.scrollHeight;
+        }
+
+        function switchTheme(themeName) {
+            // Remove all theme classes
+            Object.keys(AVAILABLE_THEMES).forEach(theme => {
+                document.body.classList.remove(`theme-${theme}`);
+            });
+            
+            // Add the new theme class
+            document.body.classList.add(`theme-${themeName}`);
+            
+            // Update meta theme-color
+            const metaThemeColor = document.querySelector('meta[name="theme-color"]');
+            if (metaThemeColor) {
+                switch(themeName) {
+                    case 'molly-millions':
+                        metaThemeColor.setAttribute('content', '#00ff00');
+                        break;
+                    case 'cloud':
+                        metaThemeColor.setAttribute('content', '#4080ff');
+                        break;
+                    case 'classic':
+                        metaThemeColor.setAttribute('content', '#DDDDDD');
+                        break;
+                    case 'lcars':
+                        metaThemeColor.setAttribute('content', '#CC6699');
+                        break;
+                    case 'professional':
+                    default:
+                        metaThemeColor.setAttribute('content', '#007BFF');
+                        break;
+                }
+            }
+            
+            localStorage.setItem('selectedTheme', themeName);            
+            addMessage(`Theme switched to: ${AVAILABLE_THEMES[themeName]}`, "bot");
+        }
+
+        // Initialize with localConfig
+        Object.assign(config, localConfig);
+    </script>
+</body>
+</html>
diff --git a/html/matt-chat/pokemon.js b/html/matt-chat/pokemon.js
new file mode 100644
index 0000000..e707e7b
--- /dev/null
+++ b/html/matt-chat/pokemon.js
@@ -0,0 +1,157 @@
+// Pokemon API functionality using functional programming approach
+
+// Base URL for the PokeAPI
+const POKE_API_BASE = 'https://pokeapi.co/api/v2';
+
+// Utility function to fetch data from the API
+const fetchPokeData = async (endpoint) => {
+    try {
+        const response = await fetch(`${POKE_API_BASE}${endpoint}`);
+        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+        return await response.json();
+    } catch (error) {
+        console.error('Error fetching Pokemon data:', error);
+        throw error;
+    }
+};
+
+// Function to get Pokemon basic info
+const getPokemonInfo = async (pokemonName) => {
+    try {
+        const data = await fetchPokeData(`/pokemon/${pokemonName.toLowerCase()}`);
+        return {
+            name: data.name,
+            id: data.id,
+            types: data.types.map(type => type.type.name),
+            abilities: data.abilities.map(ability => ({
+                name: ability.ability.name,
+                isHidden: ability.is_hidden
+            })),
+            stats: data.stats.map(stat => ({
+                name: stat.stat.name,
+                value: stat.base_stat
+            })),
+            height: data.height / 10, // Convert to meters
+            weight: data.weight / 10, // Convert to kilograms
+            sprite: data.sprites.front_default
+        };
+    } catch (error) {
+        throw new Error(`Could not find Pokemon: ${pokemonName}`);
+    }
+};
+
+// Function to get ability details
+const getAbilityInfo = async (abilityName) => {
+    try {
+        const data = await fetchPokeData(`/ability/${abilityName.toLowerCase()}`);
+        return {
+            name: data.name,
+            effect: data.effect_entries.find(e => e.language.name === 'en')?.effect || 'No effect description available.',
+            pokemon: data.pokemon.map(p => p.pokemon.name)
+        };
+    } catch (error) {
+        throw new Error(`Could not find ability: ${abilityName}`);
+    }
+};
+
+// Function to get move details
+const getMoveInfo = async (moveName) => {
+    try {
+        const data = await fetchPokeData(`/move/${moveName.toLowerCase()}`);
+        return {
+            name: data.name,
+            type: data.type.name,
+            power: data.power,
+            accuracy: data.accuracy,
+            pp: data.pp,
+            effect: data.effect_entries.find(e => e.language.name === 'en')?.effect || 'No effect description available.'
+        };
+    } catch (error) {
+        throw new Error(`Could not find move: ${moveName}`);
+    }
+};
+
+const getEvolutionInfo = async (pokemonName) => {
+    const data = await fetchPokeData(`/pokemon-species/${pokemonName.toLowerCase()}`);
+    return data.evolution_chain;
+};
+
+// Function to format Pokemon info into a readable message
+const formatPokemonInfo = (info) => {
+    const spriteImage = info.sprite ? `<img src="${info.sprite}" alt="${info.name} sprite" style="width: 100px; height: auto;" />` : '';
+    return `
+πŸ” Pokemon: ${info.name.toUpperCase()} (#${info.id})
+πŸ“Š Types: ${info.types.join(', ')}
+πŸ’ͺ Abilities: ${info.abilities.map(a => `${a.name}${a.isHidden ? ' (Hidden)' : ''}`).join(', ')}
+πŸ“ˆ Stats:
+${info.stats.map(s => `  ${s.name}: ${s.value}`).join('\n')}
+πŸ“ Height: ${info.height}m
+βš–οΈ Weight: ${info.weight}kg
+${spriteImage}
+    `.trim();
+};
+
+// Function to format ability info into a readable message
+const formatAbilityInfo = (info) => {
+    return `
+πŸ”° Ability: ${info.name.toUpperCase()}
+πŸ“ Effect: ${info.effect}
+✨ Pokemon with this ability: ${info.pokemon.join(', ')}
+    `.trim();
+};
+
+// Function to format move info into a readable message
+const formatMoveInfo = (info) => {
+    return `
+βš”οΈ Move: ${info.name.toUpperCase()}
+🎯 Type: ${info.type}
+πŸ’₯ Power: ${info.power || 'N/A'}
+🎲 Accuracy: ${info.accuracy || 'N/A'}
+πŸ”„ PP: ${info.pp}
+πŸ“ Effect: ${info.effect}
+    `.trim();
+};
+
+const formatEvolutionInfo = (info) => {
+    return `
+πŸ”— Evolution Chain: ${info.name.toUpperCase()}
+    `.trim();
+};
+
+// Main handler for Pokemon commands
+const handlePokemonCommand = async (args) => {
+    if (!args.length) {
+        return "Usage: /pokemon [pokemon|ability|move] [name]";
+    }
+
+    const [type, ...nameArgs] = args;
+    const name = nameArgs.join(' ').replace(/\s+/g, '-'); // Replace spaces with hyphens
+
+    if (!name) {
+        return "Please provide a name to search for.";
+    }
+
+    try {
+        switch (type.toLowerCase()) {
+            case 'pokemon':
+                const pokemonInfo = await getPokemonInfo(name);
+                return formatPokemonInfo(pokemonInfo);
+            case 'ability':
+                const abilityInfo = await getAbilityInfo(name);
+                return formatAbilityInfo(abilityInfo);
+            case 'move':
+                const moveInfo = await getMoveInfo(name);
+                return formatMoveInfo(moveInfo);
+            case 'evolution-chain':
+                const evolutionInfo = await getEvolutionInfo(name);
+                return formatEvolutionInfo(evolutionInfo);
+            default:
+                return "Invalid type. Use: pokemon, ability, or move.";
+        }
+    } catch (error) {
+        return `Error: ${error.message}`;
+    }
+};
+
+// Export the handler for use in main application
+export { handlePokemonCommand }; 
\ No newline at end of file
diff --git a/html/matt-chat/server.sh b/html/matt-chat/server.sh
new file mode 100755
index 0000000..b294acd
--- /dev/null
+++ b/html/matt-chat/server.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+# check that the ollama server is running, if it isn't, start it in the background and continue with the script
+if ! pgrep -f ollama; then
+    ollama start &
+fi
+
+# check that port 38478 is free
+if lsof -i :38478; then
+    echo "Port 38478 is already in use. Please choose a different port."
+    exit 1
+fi
+
+# Start a simple HTTP server using Python on port 38478 and run it in the background
+python3 -m http.server 38478 &
+
+
+#   nvim ~/Library/LaunchAgents/com.user.server.plist
+#   cp com.user.server.plist ~/Library/LaunchAgents/
+#   launchctl load ~/Library/LaunchAgents/com.user.server.plist
+#   launchctl start com.user.server
+#   launchctl list | grep com.user.server
+#   launchctl unload ~/Library/LaunchAgents/com.user.server.plist
\ No newline at end of file
diff --git a/html/read-write/index.html b/html/read-write/index.html
new file mode 100644
index 0000000..0dd75b5
--- /dev/null
+++ b/html/read-write/index.html
@@ -0,0 +1,198 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>For Akkartik & Konrad</title>
+    <style>
+        body {
+            font-family: Arial, sans-serif;
+            max-width: 800px;
+            margin: 0 auto;
+            padding: 20px;
+            background-color: beige;
+        }
+        #imagePreview {
+            max-width: 100%;
+            margin: 20px 0;
+        }
+        .controls {
+            margin: 20px 0;
+        }
+        button {
+            padding: 10px 20px;
+            margin: 5px;
+        }
+        button:hover {
+            cursor: pointer;
+        }
+    </style>
+</head>
+<body>
+    <h1>For Akkartik & Konrad</h1>
+    <p>A demo of how to edit a file in place using the <a href="https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API">File System Access API</a>.</p>
+    <p>At a quick glance, it doesn't seem to be available in Firefox or WebKit, just Chromium browsers.</p>
+    <div class="controls">
+        <button id="selectFile">Select a png</button>
+        <button id="saveChanges" disabled>Save changes</button>
+        <input type="file" id="fileInput" accept=".png" style="display: none;">
+    </div>
+
+    <img id="imagePreview" alt="Preview">
+
+    <div class="controls">
+        <button id="invertColors" disabled>Invert colors</button>
+        <button id="grayscale" disabled>Convert to greyscale</button>
+    </div>
+
+    <script>
+        // Store the file handle for later
+        let fileHandle = null;
+        let currentImageData = null;
+
+        // Get DOM elements
+        const selectButton = document.getElementById('selectFile');
+        const saveButton = document.getElementById('saveChanges');
+        const invertButton = document.getElementById('invertColors');
+        const grayscaleButton = document.getElementById('grayscale');
+        const imagePreview = document.getElementById('imagePreview');
+
+        selectButton.addEventListener('click', async () => {
+            try {
+                if ('showOpenFilePicker' in window) {
+                    // Modern File System Access API approach for browsers that support it
+                    // This will open a file picker and return a file handle
+                    fileHandle = await window.showOpenFilePicker({
+                        types: [{
+                            description: 'PNG Files',
+                            accept: {
+                                'image/png': ['.png']
+                            }
+                        }]
+                    });
+
+                    // Get the file object from the handle
+                    const file = await fileHandle[0].getFile();
+                    handleSelectedFile(file);
+                } else {
+                    // Fallback for browsers that don't support the API
+                    document.getElementById('fileInput').click();
+                }
+            } catch (err) {
+                console.error('Error selecting file:', err);
+            }
+        });
+
+        // Event listener for the fallback file input
+        document.getElementById('fileInput').addEventListener('change', async (e) => {
+            const file = e.target.files[0];
+            if (file) {
+                handleSelectedFile(file);
+                // When in fallback mode, we download a new file, rather than saving in place
+                saveButton.textContent = 'Download edited image';
+            }
+        });
+
+        async function handleSelectedFile(file) {
+            // Create a URL for the image preview
+            const imageUrl = URL.createObjectURL(file);
+            imagePreview.src = imageUrl;
+
+            // Load the image data into a canvas for editing
+            await loadImageData(file);
+
+            // Enable the editing buttons
+            invertButton.disabled = false;
+            grayscaleButton.disabled = false;
+            saveButton.disabled = false;
+        }
+
+        // Load image data into canvas
+        async function loadImageData(file) {
+            return new Promise((resolve) => {
+                const img = new Image();
+                img.onload = () => {
+                    const canvas = document.createElement('canvas');
+                    canvas.width = img.width;
+                    canvas.height = img.height;
+                    const ctx = canvas.getContext('2d');
+                    ctx.drawImage(img, 0, 0);
+                    currentImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+                    resolve();
+                };
+                img.src = URL.createObjectURL(file);
+            });
+        }
+
+        // Invert colors
+        invertButton.addEventListener('click', () => {
+            const data = currentImageData.data;
+            for (let i = 0; i < data.length; i += 4) {
+                data[i] = 255 - data[i];         // Red
+                data[i + 1] = 255 - data[i + 1]; // Green
+                data[i + 2] = 255 - data[i + 2]; // Blue
+            }
+            updatePreview();
+        });
+
+        // Convert to grayscale
+        grayscaleButton.addEventListener('click', () => {
+            const data = currentImageData.data;
+            for (let i = 0; i < data.length; i += 4) {
+                const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
+                data[i] = avg;
+                data[i + 1] = avg;
+                data[i + 2] = avg;
+            }
+            updatePreview();
+        });
+
+        function updatePreview() {
+            const canvas = document.createElement('canvas');
+            canvas.width = currentImageData.width;
+            canvas.height = currentImageData.height;
+            const ctx = canvas.getContext('2d');
+            ctx.putImageData(currentImageData, 0, 0);
+            imagePreview.src = canvas.toDataURL('image/png');
+        }
+
+        saveButton.addEventListener('click', async () => {
+            try {
+                if (fileHandle) {
+                    // Using the File System Access API
+                    const writable = await fileHandle[0].createWritable();
+                    const blob = await getImageBlob();
+                    await writable.write(blob);
+                    await writable.close();
+                    alert('Changes saved successfully!');
+                } else {
+                    // Download the edited image as a new file, if the API is not supported
+                    const blob = await getImageBlob();
+                    const downloadUrl = URL.createObjectURL(blob);
+                    const a = document.createElement('a');
+                    a.href = downloadUrl;
+                    a.download = 'edited-image.png';
+                    document.body.appendChild(a);
+                    a.click();
+                    document.body.removeChild(a);
+                    URL.revokeObjectURL(downloadUrl);
+                }
+            } catch (err) {
+                console.error('Error saving the file:', err);
+            }
+        });
+
+        async function getImageBlob() {
+            const canvas = document.createElement('canvas');
+            canvas.width = currentImageData.width;
+            canvas.height = currentImageData.height;
+            const ctx = canvas.getContext('2d');
+            ctx.putImageData(currentImageData, 0, 0);
+            
+            return new Promise(resolve => {
+                canvas.toBlob(resolve, 'image/png');
+            });
+        }
+    </script>
+</body>
+</html>
\ No newline at end of file