v1.0 unless something breaks in which case this was a beta
This commit is contained in:
parent
764f23b7bb
commit
1399db073d
|
@ -1 +1,4 @@
|
|||
config/.env
|
||||
my_solutions/
|
||||
puzzles.toml
|
||||
*.sqlite3
|
||||
|
|
3
Gemfile
3
Gemfile
|
@ -3,9 +3,12 @@ source "https://rubygems.org"
|
|||
gem "dotenv"
|
||||
|
||||
gem "activerecord"
|
||||
gem "sqlite3"
|
||||
|
||||
gem "activesupport"
|
||||
|
||||
gem "activejob"
|
||||
|
||||
gem "discordrb"
|
||||
|
||||
gem "pry"
|
||||
|
|
55
Gemfile.lock
55
Gemfile.lock
|
@ -1,18 +1,30 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activemodel (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activerecord (7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activesupport (7.0.7)
|
||||
activejob (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
activerecord (7.1.3.2)
|
||||
activemodel (= 7.1.3.2)
|
||||
activesupport (= 7.1.3.2)
|
||||
timeout (>= 0.4.0)
|
||||
activesupport (7.1.3.2)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.1.6)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.2.2)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.4.1)
|
||||
discordrb (3.5.0)
|
||||
discordrb-webhooks (~> 3.5.0)
|
||||
ffi (>= 1.9.24)
|
||||
|
@ -21,21 +33,25 @@ GEM
|
|||
websocket-client-simple (>= 0.3.0)
|
||||
discordrb-webhooks (3.5.0)
|
||||
rest-client (>= 2.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (3.1.0)
|
||||
drb (2.2.0)
|
||||
ruby2_keywords
|
||||
event_emitter (0.2.6)
|
||||
ffi (1.15.5)
|
||||
ffi (1.16.3)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.5.0)
|
||||
mime-types (3.5.2)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
minitest (5.19.0)
|
||||
mime-types-data (3.2024.0206)
|
||||
minitest (5.22.2)
|
||||
mutex_m (0.2.0)
|
||||
netrc (0.11.0)
|
||||
opus-ruby (1.0.1)
|
||||
ffi
|
||||
|
@ -47,25 +63,28 @@ GEM
|
|||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
ruby2_keywords (0.0.5)
|
||||
sqlite3 (1.7.2-x86_64-darwin)
|
||||
timeout (0.4.1)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8.2)
|
||||
websocket (1.2.9)
|
||||
websocket (1.2.10)
|
||||
websocket-client-simple (0.8.0)
|
||||
event_emitter
|
||||
websocket
|
||||
|
||||
PLATFORMS
|
||||
x86_64-darwin-21
|
||||
x86_64-darwin-23
|
||||
|
||||
DEPENDENCIES
|
||||
activejob
|
||||
activerecord
|
||||
activesupport
|
||||
discordrb
|
||||
dotenv
|
||||
pry
|
||||
sqlite3
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.13
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# Puzzle Format
|
||||
|
||||
text, a string with the text of the puzzle
|
||||
solution, a string to compare
|
||||
check_solution, a string; a ruby expression that will evaluate to true or false. `discord_username` and `submission` are in scope.
|
||||
|
||||
One of solution or check_solution must be present.
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,32 @@
|
|||
require "active_record"
|
||||
|
||||
ActiveRecord::Base.establish_connection(
|
||||
adapter: 'sqlite3',
|
||||
database: 'db.sqlite3'
|
||||
)
|
||||
|
||||
# Migrations Boilerplate
|
||||
Dir["migrations/*.rb"].each {|f| require_relative f}
|
||||
unless ActiveRecord::Base.connection.table_exists? :puzzles
|
||||
CreatePuzzleTable.migrate(:up)
|
||||
end
|
||||
|
||||
# Puzzle model
|
||||
class Puzzle < ActiveRecord::Base
|
||||
# .string :text, The markdown text of the message
|
||||
|
||||
# One of these must be present
|
||||
# table.string :solution, null: true # A string to compare against
|
||||
# table.string :check_solution, null: true # a ruby expression that will evaluate to true or false. `discord_username` and `submission` are in scope.
|
||||
|
||||
# # Public_id or title, used to identify the puzzle when upserting toml
|
||||
# table.string :public_id
|
||||
|
||||
# # used / current / ready / draft
|
||||
# table.integer :state, default: 0
|
||||
enum :state, %i[draft ready current used]
|
||||
|
||||
def validate
|
||||
return solution.present? || check_solution.present
|
||||
end
|
||||
end
|
84
main.rb
84
main.rb
|
@ -1,3 +1,5 @@
|
|||
require "pp"
|
||||
|
||||
require "dotenv"
|
||||
Dotenv.load("config/.env")
|
||||
|
||||
|
@ -5,6 +7,9 @@ require "discordrb"
|
|||
|
||||
require "active_support/all"
|
||||
|
||||
require_relative "send_puzzle_job"
|
||||
require_relative "db"
|
||||
|
||||
$bot = Discordrb::Bot.new(token: ENV["DISCORD_TOKEN"], client_id: ENV["DISCORD_CLIENT_ID"])
|
||||
|
||||
## Setup
|
||||
|
@ -13,6 +18,9 @@ at_exit { $bot.stop }
|
|||
|
||||
$server_id, $server = $bot.servers.find { |id, s| s.name == "The Linoleum Club" }
|
||||
$puzzle_solver_role = $server.roles.find { |r| r.name == "Puzzle Solver" }
|
||||
$puzzle_channel = $server.channels.find { |c| c.name == "puzzle" }
|
||||
$study_channel = $server.channels.find { |c| c.name == "sylvains-study" }
|
||||
$me = $server.users.find { |u| u.username == "matthiasportzel" }
|
||||
|
||||
def remove_solver_role
|
||||
p "remove solver role"
|
||||
|
@ -61,6 +69,10 @@ end
|
|||
|
||||
# There's probably a better way to do this re-loading
|
||||
define_command "Remove commands", [] do |event|
|
||||
if event.user != $me
|
||||
event.respond "No you did-aint!", ephemeral: false
|
||||
return
|
||||
end
|
||||
p "Removing all commands"
|
||||
|
||||
# Discord displays "Linoleum Club is thinking..." until we edit the response
|
||||
|
@ -75,7 +87,30 @@ end
|
|||
define_command("Solve", [
|
||||
{ type: :string, name: "solution", description: "Your solution to the puzzle", required: true }
|
||||
]) do |event|
|
||||
if event.options["solution"]&.strip == "1193" then
|
||||
submission = event.options["solution"]
|
||||
current_puzzle = Puzzle.current.first
|
||||
|
||||
if !submission.present?
|
||||
event.respond content: "That's not an answer"
|
||||
return
|
||||
end
|
||||
|
||||
solved = false
|
||||
if current_puzzle.solution.present?
|
||||
solved = current_puzzle.solution == submission.strip
|
||||
elsif current_puzzle.check_solution.present?
|
||||
discord_username = event.user.username
|
||||
unless discord_username.present?
|
||||
$study_channel.send "User with no username needs manual handling"
|
||||
return
|
||||
end
|
||||
# submission is also in scope, from above
|
||||
solved = eval current_puzzle.check_solution, binding
|
||||
else
|
||||
raise "No way to handle this puzzle"
|
||||
end
|
||||
|
||||
if solved then
|
||||
event.user.add_role $puzzle_solver_role
|
||||
event.respond content: "Correct! You've gained full access for this week.", ephemeral: true
|
||||
else
|
||||
|
@ -83,6 +118,51 @@ define_command("Solve", [
|
|||
end
|
||||
end
|
||||
|
||||
$irb_sessions = []
|
||||
|
||||
# Join the bot thread, so we don't exit until the bot does
|
||||
class DiscordIRBSession
|
||||
attr_accessor :user
|
||||
attr_accessor :channel
|
||||
attr_accessor :enviroment
|
||||
end
|
||||
|
||||
define_command("IRB Discord", []) do |event|
|
||||
if event.user == $me
|
||||
event.respond content: "Started an IRB-Discord session. Use `exit` to exit."
|
||||
session = DiscordIRBSession.new
|
||||
session.user = event.user
|
||||
session.channel = event.channel
|
||||
session.enviroment = binding
|
||||
$irb_sessions.push session
|
||||
else
|
||||
event.respond content: "You don't have permission to use this command"
|
||||
end
|
||||
end
|
||||
|
||||
$bot.message(from: $me) do |event|
|
||||
session = $irb_sessions.filter { |s| s.user == event.user and s.channel == event.channel }
|
||||
if session.length == 1
|
||||
session = session.first
|
||||
code = event.message.content
|
||||
if code.strip == "exit"
|
||||
$irb_sessions.delete session
|
||||
event.channel.send "Thanks for irb-ing ;)"
|
||||
else
|
||||
begin
|
||||
res = eval code, session.enviroment
|
||||
rescue => err
|
||||
res = err
|
||||
ensure
|
||||
event.channel.send "```rb\n#{PP.pp(res, "")}\n```"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get ready to send the next puzzle at the next UTC sunday
|
||||
SendPuzzleJob.new.enqueue(wait_until: Date.today.next_occurring(:sunday).beginning_of_day)
|
||||
|
||||
# binding.irb
|
||||
|
||||
# Wait forever for the bot to exit
|
||||
$bot.join
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# TODO: finishing writing and migrated
|
||||
|
||||
class CreatePuzzleTable < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :puzzles do |table|
|
||||
# The markdown text of the message
|
||||
table.string :text
|
||||
|
||||
# One of these must be present
|
||||
table.string :solution, null: true
|
||||
table.string :check_solution, null: true
|
||||
|
||||
# We need a public_id or title that I can use to identify the puzzle when upserting toml
|
||||
table.string :public_id
|
||||
|
||||
# used / current / ready / draft
|
||||
table.integer :state, default: 0
|
||||
|
||||
table.timestamps
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,108 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/all"
|
||||
require "pry"
|
||||
|
||||
class Set
|
||||
alias :includes? :include?
|
||||
end
|
||||
|
||||
class State
|
||||
def initialize(flat)
|
||||
@flat = flat
|
||||
end
|
||||
|
||||
attr_reader :flat
|
||||
|
||||
def square
|
||||
@square ||= @flat.in_groups_of 4
|
||||
end
|
||||
|
||||
# Return the index of the empty square, 0-15
|
||||
def empty
|
||||
@empty ||= flat.index 0
|
||||
end
|
||||
|
||||
def slide(dir)
|
||||
# Let's assume the move is possible
|
||||
if dir == :up
|
||||
# index of the tile to move, so that's the tile below the current pos
|
||||
# So we're going to grab empty and add three to push it to the next row
|
||||
# And that's all good as long as this is a valid move
|
||||
to_move_index = empty + 4
|
||||
elsif dir == :down
|
||||
to_move_index = empty - 4
|
||||
elsif dir == :left
|
||||
# Only valid if empty is in the first three columns
|
||||
# So the index + 1 should work
|
||||
to_move_index = empty + 1
|
||||
elsif dir == :right
|
||||
to_move_index = empty - 1
|
||||
end
|
||||
|
||||
new_state = flat.dup
|
||||
|
||||
to_move = flat[to_move_index]
|
||||
|
||||
new_state[empty] = to_move
|
||||
new_state[to_move_index] = 0
|
||||
|
||||
return State.new new_state
|
||||
end
|
||||
|
||||
def possible_moves
|
||||
valid = []
|
||||
if empty < 12 # so 0 - 11, that is, the first three rows
|
||||
valid.push :up
|
||||
end
|
||||
if empty >= 4
|
||||
valid.push :down
|
||||
end
|
||||
# Can't go left if empty is in the last row
|
||||
if empty % 4 != 3
|
||||
valid.push :left
|
||||
end
|
||||
if empty % 4 != 0
|
||||
valid.push :right
|
||||
end
|
||||
|
||||
return valid
|
||||
end
|
||||
|
||||
def adjacent_states
|
||||
return possible_moves.map do |dir|
|
||||
slide dir
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
@s ||= flat.join ""
|
||||
end
|
||||
|
||||
def dis
|
||||
puts square.map(&:to_s).join("\n").sub("0", " ")
|
||||
puts "\n"
|
||||
end
|
||||
end
|
||||
|
||||
seen_states = Set.new
|
||||
|
||||
# Last is actually the one we're going to work off of, so we can push and pop
|
||||
unchecked_states = [State.new((0..15).to_a)]
|
||||
|
||||
while unchecked_states.length > 0
|
||||
state = unchecked_states.pop
|
||||
|
||||
state.adjacent_states.each do |s|
|
||||
unless seen_states.includes? s.to_s
|
||||
seen_states.add s.to_s
|
||||
unchecked_states.push s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# seen_states.count
|
||||
# => 181440
|
||||
|
||||
binding.pry
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// From /Users/matthias/Programs/tmp-name-js-plus/main.js
|
||||
Object.defineProperty(Array.prototype, "chunks", {
|
||||
value: function chunks (chunkSize) {
|
||||
// TODO: Error on float
|
||||
if (chunkSize<= 0) {
|
||||
throw new Error("Window size must be a positive integer.");
|
||||
}
|
||||
|
||||
if (this.length < chunkSize) {
|
||||
return this.slice(0);
|
||||
}
|
||||
|
||||
const accum = [];
|
||||
for (let i = 0; i < this.length; i += chunkSize) {
|
||||
accum.push(this.slice(i, i + chunkSize));
|
||||
}
|
||||
return accum;
|
||||
}
|
||||
});
|
||||
|
||||
const input = "W 3 N 5 I 2 W 3 E 2 R 5 O 1 I 5 S 2 S 8 O 2 I 8 W 5 W 6 W 2 W 1 E 7 O 9";
|
||||
|
||||
const i = input.split(" ").chunks(2)
|
||||
let x = 0, y = 0, z = 0;
|
||||
for (let [dir, dist] of i) {
|
||||
dist = parseInt(dist, 10);
|
||||
|
||||
if (dir === "N") {
|
||||
y -= dist;
|
||||
} else if (dir === "S") {
|
||||
y += dist;
|
||||
} else if (dir === "E") {
|
||||
x += dist;
|
||||
} else if (dir === "W") {
|
||||
x -= dist;
|
||||
} else if (dir === "I") {
|
||||
z += dist;
|
||||
} else if (dir === "O") {
|
||||
z -= dist;
|
||||
}
|
||||
}
|
||||
console.log(`(${x}, ${y}, ${z})`)
|
|
@ -0,0 +1,108 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/all"
|
||||
require "pry"
|
||||
|
||||
class Set
|
||||
alias :includes? :include?
|
||||
end
|
||||
|
||||
class State
|
||||
def initialize(flat)
|
||||
@flat = flat
|
||||
end
|
||||
|
||||
attr_reader :flat
|
||||
|
||||
def square
|
||||
@square ||= @flat.in_groups_of 3
|
||||
end
|
||||
|
||||
# Return the index of the empty square, 0-8
|
||||
def empty
|
||||
@empty ||= flat.index 0
|
||||
end
|
||||
|
||||
def slide(dir)
|
||||
# Let's assume the move is possible
|
||||
if dir == :up
|
||||
# index of the tile to move, so that's the tile below the current pos
|
||||
# So we're going to grab empty and add three to push it to the next row
|
||||
# And that's all good as long as this is a valid move
|
||||
to_move_index = empty + 3
|
||||
elsif dir == :down
|
||||
to_move_index = empty - 3
|
||||
elsif dir == :left
|
||||
# Only valid if empty is in the first two columns
|
||||
# So the index + 1 should work
|
||||
to_move_index = empty + 1
|
||||
elsif dir == :right
|
||||
to_move_index = empty - 1
|
||||
end
|
||||
|
||||
new_state = flat.dup
|
||||
|
||||
to_move = flat[to_move_index]
|
||||
|
||||
new_state[empty] = to_move
|
||||
new_state[to_move_index] = 0
|
||||
|
||||
return State.new new_state
|
||||
end
|
||||
|
||||
def possible_moves
|
||||
valid = []
|
||||
if empty <= 5
|
||||
valid.push :up
|
||||
end
|
||||
if empty >= 3
|
||||
valid.push :down
|
||||
end
|
||||
# Can't go left if empty is in the last row
|
||||
if empty % 3 != 2
|
||||
valid.push :left
|
||||
end
|
||||
if empty % 3 != 0
|
||||
valid.push :right
|
||||
end
|
||||
|
||||
return valid
|
||||
end
|
||||
|
||||
def adjacent_states
|
||||
return possible_moves.map do |dir|
|
||||
slide dir
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
@s ||= flat.join ""
|
||||
end
|
||||
|
||||
def dis
|
||||
puts square.map(&:to_s).join("\n").sub("0", " ")
|
||||
puts "\n"
|
||||
end
|
||||
end
|
||||
|
||||
seen_states = Set.new
|
||||
|
||||
# Last is actually the one we're going to work off of, so we can push and pop
|
||||
unchecked_states = [State.new((0..8).to_a)]
|
||||
|
||||
while unchecked_states.length > 0
|
||||
state = unchecked_states.pop
|
||||
|
||||
state.adjacent_states.each do |s|
|
||||
unless seen_states.includes? s.to_s
|
||||
seen_states.add s.to_s
|
||||
unchecked_states.push s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# seen_states.count
|
||||
# => 181440
|
||||
|
||||
binding.pry
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 7.3 MiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 28 KiB |
|
@ -0,0 +1,18 @@
|
|||
require "active_support/all"
|
||||
|
||||
first_sundays = 0
|
||||
|
||||
start_date = Date.parse "1901-01-01"
|
||||
end_date = Date.parse "2000-12-31"
|
||||
|
||||
date = start_date
|
||||
|
||||
while date < end_date
|
||||
if date.day == 1 and date.sunday? then
|
||||
first_sundays += 1
|
||||
end
|
||||
|
||||
date = date.tomorrow
|
||||
end
|
||||
|
||||
p first_sundays
|
|
@ -0,0 +1,15 @@
|
|||
require "vips"
|
||||
|
||||
im = Vips::Image.new_from_file "Linoleum.jpg"
|
||||
|
||||
binding.irb
|
||||
|
||||
count = 0
|
||||
im.to_a.each do |row|
|
||||
row.each do |pixel|
|
||||
count += 1 if pixel == [52, 49, 53]
|
||||
# p pixel if pixel[0] == 71
|
||||
end
|
||||
end
|
||||
|
||||
p count
|
|
@ -0,0 +1,208 @@
|
|||
# Authored 2023 Matthias Portzel. Public Domain
|
||||
|
||||
require "pry"
|
||||
require "active_support/all"
|
||||
require "ostruct"
|
||||
|
||||
class Direction
|
||||
attr_accessor :name
|
||||
attr_accessor :offset
|
||||
|
||||
def initialize(name, offset)
|
||||
@name = name
|
||||
@offset = OpenStruct.new x: offset[0], y: offset[1]
|
||||
end
|
||||
end
|
||||
|
||||
DIRECTIONS = [
|
||||
Direction.new(:up_left, [-1, -1]),
|
||||
Direction.new(:up, [0, -1]),
|
||||
Direction.new(:up_right, [1, -1]),
|
||||
Direction.new(:left, [-1, 0]),
|
||||
Direction.new(:right, [1, 0]),
|
||||
Direction.new(:down_left, [-1, +1]),
|
||||
Direction.new(:down, [0, +1]),
|
||||
Direction.new(:down_right, [+1, +1])
|
||||
]
|
||||
|
||||
def for_xy (x_range, y_range, &block)
|
||||
for x in x_range
|
||||
for y in y_range
|
||||
block.call x, y
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Cell
|
||||
attr_accessor :is_alive
|
||||
def alive?
|
||||
@is_alive
|
||||
end
|
||||
def dead?
|
||||
!@is_alive
|
||||
end
|
||||
|
||||
for d in DIRECTIONS
|
||||
attr_accessor d.name
|
||||
end
|
||||
|
||||
attr_accessor :coords
|
||||
|
||||
def initialize(is_alive)
|
||||
@is_alive = is_alive
|
||||
@coords = OpenStruct.new x: nil, y: nil
|
||||
end
|
||||
|
||||
def neighbors
|
||||
DIRECTIONS.map do |d|
|
||||
send d.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class MobiusStrip
|
||||
# 2D array of trues and falses
|
||||
def initialize(grid)
|
||||
@state = grid.map do |row|
|
||||
row.map do |cell|
|
||||
Cell.new cell
|
||||
end
|
||||
end
|
||||
|
||||
max_x = @state.first.length
|
||||
max_y = @state.length
|
||||
|
||||
for_xy (0..(max_x-1)), (0..(max_y-1)) do |x, y|
|
||||
cell = @state[y][x]
|
||||
cell.coords.x = x
|
||||
cell.coords.y = y
|
||||
for d in DIRECTIONS
|
||||
link_x = (x + d.offset.x) % max_x
|
||||
link_y = (y + d.offset.y) % max_y
|
||||
if x + d.offset.x == -1 || x + d.offset.x == max_x
|
||||
link_y = ((max_y - 1) - link_y) % max_y
|
||||
end
|
||||
|
||||
cell.send(d.name.to_s + "=", @state[link_y][link_x])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def grid
|
||||
@state
|
||||
end
|
||||
|
||||
def self.from_string(grid_string)
|
||||
new(grid_string.split("\n").map(&:strip).map do |row|
|
||||
row.split("").map do |cell|
|
||||
cell == "#"
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def count_living_neighbors(cell)
|
||||
cell.neighbors.filter(&:alive?).length
|
||||
end
|
||||
|
||||
def step
|
||||
max_x = @state.first.length
|
||||
max_y = @state.length
|
||||
|
||||
new_grid = Array.new(max_y).map { Array.new(max_x) }
|
||||
for_xy (0..(max_x-1)), (0..(max_y-1)) do |x, y|
|
||||
cell = @state[y][x]
|
||||
live_neighbors = count_living_neighbors(@state[y][x])
|
||||
|
||||
# Cell alive, and 0..1 live neighbors, die
|
||||
# Cell alive, 4.. live neighbors, die
|
||||
# Cell alive, no change
|
||||
# Cell dead, 3 live neighbors, live
|
||||
# Cell dead, no change
|
||||
new_state = if cell.alive? && (0..1) === live_neighbors then
|
||||
false
|
||||
elsif cell.alive? && live_neighbors >= 4 then
|
||||
false
|
||||
elsif cell.alive? then
|
||||
true
|
||||
elsif cell.dead? && live_neighbors == 3 then
|
||||
true
|
||||
elsif cell.dead?
|
||||
false
|
||||
end
|
||||
|
||||
# p [cell.coords, cell.alive?.to_s, live_neighbors.to_s, new_state.to_s].join ", "
|
||||
|
||||
new_grid[y][x] = new_state
|
||||
end
|
||||
|
||||
MobiusStrip.new new_grid
|
||||
end
|
||||
|
||||
def display
|
||||
lines = @state.map do |row|
|
||||
chars = row.map do |cell|
|
||||
if cell.alive? then "#" else "." end
|
||||
end
|
||||
chars.join ""
|
||||
end
|
||||
lines.join "\n"
|
||||
end
|
||||
end
|
||||
|
||||
test_board = MobiusStrip.from_string(<<-EOF
|
||||
..........
|
||||
..........
|
||||
........#.
|
||||
........#.
|
||||
#.#..#..#.
|
||||
#.#.#.#...
|
||||
.##..##.#.
|
||||
..#.......
|
||||
##........
|
||||
..........
|
||||
EOF
|
||||
)
|
||||
|
||||
board = MobiusStrip.from_string(<<-EOF
|
||||
....................
|
||||
............###.....
|
||||
.....#.#...#...#....
|
||||
...........#........
|
||||
.....#.#...#........
|
||||
......#....#.###....
|
||||
...........#...#....
|
||||
...##......#...#....
|
||||
...##.......###.....
|
||||
....................
|
||||
....................
|
||||
...#####...#...#....
|
||||
...#.......#...#....
|
||||
...#.......#...#....
|
||||
...#####...#####....
|
||||
...#.......#...#....
|
||||
...#.......#...#....
|
||||
...#.......#...#....
|
||||
...#.......#...#....
|
||||
....................
|
||||
EOF
|
||||
)
|
||||
|
||||
# board = test_board
|
||||
past_boards = []
|
||||
day = 0
|
||||
loop do
|
||||
board_string = board.display
|
||||
|
||||
if past_boards.include? board_string
|
||||
after_days = day - 1
|
||||
puts "Repetition after day #{after_days}"
|
||||
break
|
||||
end
|
||||
|
||||
puts "On day #{day}:"
|
||||
puts board_string
|
||||
|
||||
past_boards << board_string
|
||||
board = board.step
|
||||
day += 1
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
const a = {b: 2, c: 3};
|
||||
|
||||
Object.defineProperty(Object.prototype, "with_key_methods", {
|
||||
value: function () {
|
||||
for (const {k: v} in this) {
|
||||
console.log(k, v);
|
||||
// this[k] = () => v;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
a.with_key_methods().b()
|
|
@ -0,0 +1 @@
|
|||
{"type":"module"}
|
|
@ -0,0 +1,37 @@
|
|||
// What is the second smallest prime number:
|
||||
// * whose digits are all odd,
|
||||
// * greater than 1057
|
||||
// * whose palindrome is also an odd number
|
||||
|
||||
const primes = [];
|
||||
|
||||
function isPrime(num) {
|
||||
if (primes.includes(num)) {
|
||||
return true;
|
||||
}else {
|
||||
const s = sqrt(num);
|
||||
for (const i = 0; i < s; i++) {
|
||||
if (num % i === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
primes.push(num);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Number.prototype.digits = function () {
|
||||
return this.toString().split().reverse().map(d => parseInt(d, 10));
|
||||
}
|
||||
|
||||
Number.prototype.isOdd = function () {
|
||||
return this % 2 === 1;
|
||||
}
|
||||
|
||||
const numFound = 0;
|
||||
for (const i = 1057; numFound < 2; i++) {
|
||||
|
||||
if (isPrime(i) && i.digits().all()) {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
require 'prime'
|
||||
|
||||
# // What is the second smallest prime number:
|
||||
# // * whose digits are all odd,
|
||||
# // * greater than 1057
|
||||
# // * whose palindrome is also an odd number
|
||||
|
||||
numFound = 0
|
||||
i = 1057
|
||||
while numFound < 30 do
|
||||
if i.prime? && i.digits.join.to_i.prime?
|
||||
p i
|
||||
numFound += 1
|
||||
end
|
||||
|
||||
i += 1
|
||||
end
|
|
@ -0,0 +1,182 @@
|
|||
// Going to regret doing this in JS but whatever
|
||||
|
||||
import "/Users/matthias/Programs/tmp-name-js-plus/main.js"
|
||||
|
||||
/*
|
||||
const points = [];
|
||||
for (let i = 0; i < 33; i++) {
|
||||
points.push({
|
||||
x: Math.floor(Math.random() * 100),
|
||||
y: Math.floor(Math.random() * 100)
|
||||
});
|
||||
}
|
||||
const seen = new Set();
|
||||
for (const point of points) {
|
||||
seen.add(JSON.stringify(point));
|
||||
}
|
||||
points.clear();
|
||||
for (const point of seen) {
|
||||
points.push(JSON.parse(point));
|
||||
}
|
||||
*/
|
||||
const points = [
|
||||
{ x: 21, y: 21 }, { x: 94, y: 81 },
|
||||
{ x: 22, y: 32 }, { x: 98, y: 96 },
|
||||
{ x: 12, y: 94 }, { x: 57, y: 58 },
|
||||
{ x: 31, y: 30 }, { x: 56, y: 7 },
|
||||
{ x: 60, y: 1 }, { x: 27, y: 47 },
|
||||
{ x: 74, y: 55 }, { x: 53, y: 70 },
|
||||
{ x: 48, y: 74 }, { x: 20, y: 68 },
|
||||
// { x: 47, y: 70 }, { x: 51, y: 27 },
|
||||
// { x: 93, y: 27 }, { x: 31, y: 88 },
|
||||
// { x: 25, y: 36 }, { x: 28, y: 31 },
|
||||
// { x: 44, y: 33 }, { x: 18, y: 56 },
|
||||
// { x: 80, y: 18 }, { x: 44, y: 65 },
|
||||
// { x: 0, y: 61 }, { x: 57, y: 55 },
|
||||
// { x: 39, y: 29 }, { x: 4, y: 39 },
|
||||
// { x: 83, y: 59 }, { x: 9, y: 38 },
|
||||
// { x: 28, y: 2 }, { x: 28, y: 73 },
|
||||
// { x: 72, y: 40 }
|
||||
];
|
||||
|
||||
// At least 3 points in each loop
|
||||
|
||||
// A*?
|
||||
// I'm nervous because this is a really poor match for A* on the face of it; A* excels at deep problems, but this is *really* shallow. Maybe that's fine? Because it's so shallow it shouldn't be that hard to finish any particular branch.
|
||||
// But we also might hit memory limitations.
|
||||
|
||||
// My intuition tells me brute force will be just out of reach. 33! solutions? / 3 for symmetry between the 3 loops. I think. 3 e 36.
|
||||
// At least several days
|
||||
|
||||
// let l = 0
|
||||
// for (let i = 0; i < 2.894439206E36; i ++) {
|
||||
// l += i;
|
||||
// if (i % 100000000 === 0) {
|
||||
// console.log(i / 2.894439206E36)
|
||||
// }
|
||||
// }
|
||||
// console.log(l);
|
||||
|
||||
// I'm going to give A* a shot
|
||||
// A node is a configuration of assigned loops and any unassigned points
|
||||
// A*'s nice because as soon as we find one solution that assigns all points, we have the minimum
|
||||
|
||||
// A* is best-first + heuristic, with the score being the current loop distance
|
||||
// The problem is we don't have a good heuristic
|
||||
|
||||
// Let's run best-first and see what happens
|
||||
|
||||
function distance (point1_id, point2_id) {
|
||||
const point1 = points[point1_id];
|
||||
const point2 = points[point2_id];
|
||||
return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2);
|
||||
}
|
||||
|
||||
class State {
|
||||
constructor () {
|
||||
// super(); ???
|
||||
|
||||
// Three loops. Each one is a list of points
|
||||
// "point" refers to the index of the point in points, we're not copying them
|
||||
this.loops = [[], [], []];
|
||||
this.unassignedPoints = Array.range(points.length);
|
||||
// We can start off with the first point in the first loop
|
||||
this.assign(0, 0);
|
||||
}
|
||||
|
||||
assign (point_i, loop_i) {
|
||||
this.loops[loop_i].push(point_i);
|
||||
this.unassignedPoints.remove(point_i);
|
||||
this.updateScore();
|
||||
}
|
||||
|
||||
isComplete () {
|
||||
return this.unassignedPoints.length === 0;
|
||||
}
|
||||
|
||||
getIncompleteScore () {
|
||||
let score = 0;
|
||||
for (const loop of this.loops) {
|
||||
loop.windows(2).forEach(function (pair) {
|
||||
score += distance(...pair);
|
||||
});
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
getCompleteScore () {
|
||||
// The complete score is just the incomplete score + the distance to "close", from the last point to the first one
|
||||
let score = this.getIncompleteScore();
|
||||
for (const loop of loops) {
|
||||
score += distance(loop.last, loop.first);
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
// Get the score
|
||||
getScore () {
|
||||
return this.isComplete() ? this.getCompleteScore() : this.getIncompleteScore();
|
||||
}
|
||||
|
||||
updateScore () {
|
||||
this.score = this.getScore();
|
||||
}
|
||||
|
||||
clone () {
|
||||
// This creates a new initial State, but that's fine
|
||||
const state = new State();
|
||||
state.unassignedPoints = this.unassignedPoints.copy();
|
||||
for (let i = 0; i < this.loops.length; i ++) {
|
||||
state.loops[i] = this.loops[i].copy();
|
||||
}
|
||||
state.updateScore();
|
||||
return state;
|
||||
}
|
||||
|
||||
// Return all possible next states
|
||||
getNextStates () {
|
||||
// if there aren't any, return []
|
||||
if (this.isComplete()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const nextStates = [];
|
||||
// Otherwise, we have the option of assigning any unassigned point to any of the three loops
|
||||
for (let i = 0; i < this.unassignedPoints.length; i++) {
|
||||
for (let j = 0; j < this.loops.length; j++) {
|
||||
const state = this.clone();
|
||||
state.assign(this.unassignedPoints[i], j);
|
||||
nextStates.push(state);
|
||||
}
|
||||
}
|
||||
return nextStates;
|
||||
}
|
||||
}
|
||||
|
||||
// Sorted by score of course
|
||||
// Best score at the end, so we can .pop() it
|
||||
// Best score is lowest of course
|
||||
const states = [new State()];
|
||||
|
||||
// console.log(states.first);
|
||||
// console.log(states.first.getNextStates());
|
||||
|
||||
// Insert 1 or more states preserving sort order
|
||||
function insertState (...states) {
|
||||
for (const state of states) {
|
||||
// i is the index that the state will end up at
|
||||
// If we're inserting at the end a lot, running this loop in reverse may be worth it
|
||||
let i = 0;
|
||||
while (i < states.length && states[i].score > state.score) {
|
||||
i ++;
|
||||
}
|
||||
states.splice(i, 0, state);
|
||||
}
|
||||
}
|
||||
|
||||
while (!states.last.isComplete()) {
|
||||
// process a state
|
||||
const state = states.pop();
|
||||
insertState(...state.getNextStates());
|
||||
}
|
||||
print(states);
|
|
@ -0,0 +1,24 @@
|
|||
require "nokogiri"
|
||||
|
||||
f = File.read("Linoleumherstellung.svg")
|
||||
|
||||
xml = Nokogiri::XML(f)
|
||||
|
||||
# binding.irb
|
||||
|
||||
# stack = [xml]
|
||||
# max_depth = 0
|
||||
|
||||
# until stack.empty?
|
||||
|
||||
# end
|
||||
|
||||
# Height is the number of edges, so a tree with two nodes has height 1
|
||||
# <foo> <bar> </bar> </foo>
|
||||
|
||||
def max_depth(tree)
|
||||
return 0 if tree.children.empty?
|
||||
return 1 + (tree.children.map { |c| max_depth c }).max
|
||||
end
|
||||
|
||||
p max_depth(xml)
|
|
@ -0,0 +1,24 @@
|
|||
require "active_job"
|
||||
|
||||
class SendPuzzleJob < ActiveJob::Base
|
||||
queue_as :default
|
||||
|
||||
# Do something later
|
||||
def perform
|
||||
# Remove the puzzle command from everyone
|
||||
remove_solver_role
|
||||
|
||||
# Get a random puzzle
|
||||
puzzle = Puzzle.ready.order("RANDOM()").first
|
||||
|
||||
if puzzle.present?
|
||||
# Send the new puzzle
|
||||
$puzzle_channel.send puzzle.text
|
||||
# Mark it as current
|
||||
puzzle.current!
|
||||
else
|
||||
# Panic
|
||||
$study_channel.send "<@#{$me.id}> Help! No more puzzles! No more Club???"
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue