diff options
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 |