DataWraith's recent activity

  1. Comment on One quirky anti AI technique I've used is leaving in the typos in ~tech

    DataWraith
    Link
    This post reminds me of the Loebner Prize; I hadn't thought of that in a very long time. They basically tried to conduct a Turing Test every year, and I was always interested in artificial...

    This post reminds me of the Loebner Prize; I hadn't thought of that in a very long time. They basically tried to conduct a Turing Test every year, and I was always interested in artificial intelligence, so I read everything I could about the contestants at the time.

    My memory may be fuzzy now, but I remember at least two of the competitors writing about how they added deliberate typos to their messages, so they would appear more human to the judges. Adding a delay before responding was fairly common, but I think someone even made a sophisticated model of how typos happen on a QWERTY keyboard in order to not stand out as much.

    One of my favorites to interact with at the time was MegaHAL. It was based on a Markov Chain approach, so I guess you could think of it as a very small language model, just not one based on the Transformer architecture.

    8 votes
  2. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link Parent
    It would have been nice if Kubernetes had worked out; it does bring many good things to the table, even if it is likely overkill for such a small setup. I was so curious if I could make it work...

    It would have been nice if Kubernetes had worked out; it does bring many good things to the table, even if it is likely overkill for such a small setup. I was so curious if I could make it work that I didn't really stop to think if it actually, actually makes sense for me to do so.

    But I likely wouldn't have learned about Incus if I hadn't tried to Kubernetes, so the time was not wasted, even if the decision to invest that time feels a bit stupid in retrospect.

  3. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link
    I tried to -- and failed to -- setup Kubernetes on my home-server. I was running a few things in Docker/Portainer before, but I felt like the server was getting too "messy". The platonic ideal...

    I tried to -- and failed to -- setup Kubernetes on my home-server. I was running a few things in Docker/Portainer before, but I felt like the server was getting too "messy".

    The platonic ideal would be to have a server that can be wiped and re-constituted easily if need be. Ansible would probably have worked to accomplish the goal of having a reproducible system, but I had worked with Ansible in the past and wanted to try something new.

    So, Kubernetes. Google-scale on a single machine... 🤦 (though I planned on adding more machines later on to get a proper quorum of three). Why, oh why, did I think this was a good idea?

    Talos Linux is a distribution that runs nothing but Kubernetes. You can't even SSH into the server because there are no system tools installed -- everything runs through the Kubernetes API. There are a ton of good tutorials on YouTube from the creators of Talos, and I got relatively far with my first attempt: everything needed for running containers worked (modulo a detour where there was not enough entropy in the system to derive encryption keys for the hard disk).

    But I couldn't manage to get a port forwarded to an application, specifically ports 80 and 443. Theoretically I could have installed something called MetalLB or just lived with the fact that NodePorts are generally assigned in the 30000+ range, but at that point the idiocy of the whole endeavor caught up with me.

    However, my research triggered the YouTube algorithm, and I got served videos about something called Incus. Incus is a Container/VM manager that allows very easy creation of docker containers, system containers (containing an entire operating system except for the kernel) and full virtual machines.

    So I wiped Kubernetes, threw Ubuntu Server on the server and installed Incus. It took a bit of fiddling until everything was setup to my liking, but the docs are thorough, so it was mostly smooth sailing.

    With a bit of setup you can use <container>.incus DNS names to resolve the containers from the server itself, so that different containers can talk to each other over a network bridge. I used that to my advantage, installing a Debian container with Caddy as a reverse-proxy, and now all software on there was accessible via HTTPS (using a self-signed cert).

    It is very nice that you can use different Linux distributions from the image library without having to go through a manual installation. I installed Forgejo in a NixOS container, because on NixOS that's like 10 lines of configuration to set it up, whereas most other distributions would have required manual setup. Heck, even a docker-compose for Forgejo is more complicated to setup.

    So yeah. Kubernetes was a stupid idea, but it led to Incus, which worked out fantastic so far. You can snapshot containers and VMs and copy or migrate them to other hosts as well. In theory I can wipe the server, transfer backed-up snapshots to it, and be up and running again if things ever get too messy.

    3 votes
  4. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link
    I've been obsessively working on my Notelog project. Notelog is a CLI note-taking program that also has a Model Context Protocol server built-in, so that it can be used by AI assistants. I'm now...

    I've been obsessively working on my Notelog project. Notelog is a CLI note-taking program that also has a Model Context Protocol server built-in, so that it can be used by AI assistants.

    I'm now done with all the functionality I imagined for the tool -- note creation, note searching (via SQLite's FTS5 full-text search), and editing of tags works by just asking the LLM. I'm using Gemini 2.5 Flash via Dive -- Dive is not super polished, but there does not appear to be another MCP client that can both live in my system tray and works under Linux. It's nice to be able to just chuck a "/log this-or-that" line at Gemini, and have it make a grammatically correct note of it, with automatically selected title and a few tags (though it doesn't do that 100% reliably, which is a bit annoying).

    The project now clocks in at about 5000 lines of code (mostly AI-written, though I do read and fix the code if there are issues). I think it's an example of software that is uniquely enabled by AI-coding, because writing everything myself would have taken so long that I would have abandoned the project before it got to a useful state. It's also software that is somewhat unlikely to be useful for anyone but me... I expect a lot more software that is only useful to a small group of people or single person will get written as prices go down due to LLMs getting better at writing software fast.

    One gimmick I started experimenting with is giving the AI access to my self-hosted Forgejo instance. I made a user for it, created an access token, and now it can read and comment on issues in projects I invite it to. Instead of writing a long prompt inside of the cramped IDE, I can just create (and preview!) an issue and then tell it to read and implement that -- a kind of poor man's Codex, if you will.

    9 votes
  5. Comment on Introducing Codex [OpenAI] in ~tech

    DataWraith
    Link
    Looks like GitHub is going to release their own version of this within weeks. Unlike Codex, the Copilot Agent can use Model Context Protocol servers and has access to the internet (managed using a...

    Looks like GitHub is going to release their own version of this within weeks.

    Using state-of-the-art models, the agent excels at low-to-medium complexity tasks in well-tested codebases, from adding features and fixing bugs to extending tests, refactoring code, and improving documentation. You can hand off the time-consuming, but boring tasks to Copilot that will use pull requests, CI/CD, and all of your existing tooling while you focus on the interesting work.

    Unlike Codex, the Copilot Agent can use Model Context Protocol servers and has access to the internet (managed using a whitelist).

    3 votes
  6. Comment on Introducing Codex [OpenAI] in ~tech

    DataWraith
    (edited )
    Link
    Two weeks ago I would have been very skeptical of this, but my experience with agentic coding since then makes me think that this would be a wonderful tool -- write a spec, fire off the prompt,...

    Two weeks ago I would have been very skeptical of this, but my experience with agentic coding since then makes me think that this would be a wonderful tool -- write a spec, fire off the prompt, and then wait a few minutes for it to open a Pull Request on GitHub that implements the feature you asked for, including tests and documentation.

    And while it's running, you can draft the next spec in parallel, fire off another agent, or work on the code yourself -- the latter is a bit of a pain point with my current setup, because Augment Code gets confused if you try to edit a file it wants to change itself.
    With the cloud-based setup, you also don't need to worry about an LLM going mad and deleting your home directory...

    Apart from data privacy, the most glaring downside is the price. If I'm parsing this right, it is only available on the $200/month tier for now, and they may charge extra after the initial testing period.

    Edit: Looks like it doesn't open PRs automatically, you have to click a button (for now). The recording of the livestream they have at the bottom of the page is interesting, and they've kept fluff to a minimum. Everyone is a bit awkward, but you get to see the system in action and they explain their vision for how this could evolve in the future.

    8 votes
  7. Comment on Visualising how AI training avoids getting stuck in local minima in ~comp

    DataWraith
    Link
    Welch Labs is an amazing channel in general! If you like this style of video, I highly recommend his series on Learning to see, where he teaches the basics of machine learning, how Decision Trees...

    Welch Labs is an amazing channel in general!

    If you like this style of video, I highly recommend his series on Learning to see, where he teaches the basics of machine learning, how Decision Trees work in particular, in a highly accessible manner.

    Other good ones are the series on Self-Driving cars (that I think he never finished?) and the recent video about how DeepSeek's Latent Attention works.

    5 votes
  8. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link Parent
    Thanks! I tried to make it more entertaining than usual, and I wasn't sure whether that worked or just ended up being awkward. The Notelog program I wrote (co-authored?) with the AI Agent exposes...

    Thanks! I tried to make it more entertaining than usual, and I wasn't sure whether that worked or just ended up being awkward.

    Is it able to actually create files, or do you have it just summarize your conversation and then separately you copy that into a notelog file? I'd be curious to learn more about your setup.

    The Notelog program I wrote (co-authored?) with the AI Agent exposes a Model Context Protocol server -- a program that the LLM can start and directly talk to in order to get things done.

    In my case I just tell the LLM to log a note or summarize the conversation (which LLMs are good at), and then it calls Notelog with the note or summary. Notelog looks at the title of the summary and creates an appropriately named file in my notes directory before writing the text to the file, so I don't have to copy anything myself.

    If you are on Windows or Mac, you can experiment with Claude Desktop (Tutorial) to get a feel for what is possible. The filesystem server they set up in that tutorial might already allow you to have Claude create files useful for your workflow.

    2 votes
  9. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link Parent
    The tool is now in a state that is actually somewhat useful, so I decided to just go ahead and release the source code, in case anyone is curious. A few remarks: The quality of the resulting code...

    The tool is now in a state that is actually somewhat useful, so I decided to just go ahead and release the source code, in case anyone is curious.

    A few remarks:

    • The quality of the resulting code isn't amazing, and there are way too many obvious "this line does X" comments, but as a basic prototype, it gets the job done.
    • Having so many comments makes reviewing generated code easier, because you just look at the overall flow of the comments, and then check that the code does what the comment says it does.
    • As for effort-to-effect ratio, I think it is pretty good. I doubt I could have gotten this much done within 2-3 days without the AI Agent. It doesn't make me a 10X developer, but it does provide a substantial speed-up.
    2 votes
  10. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    (edited )
    Link
    I've been writing my own note-taking tool for the past few days. There's a ton of existing options in that space, from Obsidian to org-mode, but I never managed to stick with one. So I thought...

    I've been writing my own note-taking tool for the past few days.

    There's a ton of existing options in that space, from Obsidian to org-mode, but I never managed to stick with one. So I thought "I'm a programmer, why don't I make one myself?".

    So now I have a Rust tool called notelog (imaginative name, I know!), which I can simply call from the commandline and it'll either take the note directly from the commandline args, read STDIN if provided, or open $EDITOR to let me write a longer note.
    The hardest part was getting all the error handling implemented -- there's so much that can go wrong even in such a simple program, and there's still a ton to do, though I'm now at a point where you'd have to actively try to break things for them to go south (like, by setting the $EDITOR env variable to rm -rf / or something).

    Each note is written to a Markdown file and stored in an automatically-created folder hierarchy based on the current year and month. The files all have YAML frontmatter, so the entire thing is extensible.
    As a proof of concept, the current version can store a list of tags for each note, but I plan on adding other things like "status: todo/done/delegated/canceled/etc" later on. Right now it also just stores the note; if I want to find it again, I have to use grep.

    I ended up using Generative AI to auto-generate almost all 1500 lines of it (using the AugmentCode extension for VSCode), though the process required some handholding on my part -- the AI won't refactor code on its own, and it doesn't have good 'taste' yet on when to factor out or inline functionality, or refactor in general. Or maybe that's just a skill issue with my prompting technique.

    Still, it's quite amazing that technology has progressed to the point where you can write a 50 line spec in English and get a 450 line Rust file out that does everything you outlined in the spec -- mere minutes after handoff. I had to tell it to break the generated code up into smaller modules, but I basically had the initial prototype working using that single (though thorough) prompt. The AI does a lot of legwork once it is running -- writing the code, writing tests, running tests, debugging errors, looking up documentation, cleaning up things the linter didn't like, even 'speaking' JSON-RPC to notelog in order to debug an issue with the MCP server I asked it to build for the tool.

    The MCP server allows me to use NoteLog from inside my IDE or using any other MCP client. Whenever I feel like it, I can just '/log some note with tags +foo and +bar' or have the LLM do it for me ("summarize the points we just discussed into a notelog note").

    Some people say that AGI is right around the corner, and I'm starting to believe they're right. You can just hand off tasks to the AI agent, do something else for 5 or 10 minutes, and expect it to have implemented the entire feature you've requested when you get back. (I do run the IDE in a virtual machine though, just in case it does decide to rm -rf everything). Heck, you can even ask it to come up with a draft spec for new functionality based on vague ideas, and then review the result, and eventually just tell it to "make it so".

    It won't be too long until something like it can do a whole day's work with a single guiding prompt.

    It's not all rainbows and sunshine though. I'm a bit ambivalent about how capable the darn thing is -- you just start to trust it, start glossing over the text it outputs (there's sooo much of it, though each task completed ends with a summary of what it has done), and I find myself getting less vigilant over time.

    In two later instances, the agent silently did (minor) things that were against the prompt/spec I formulated. In retrospect, the spec was wrong on these points, but ideally it shouldn't have deviated from it. At one point it got into a doom loop of trying to read the source code of a dependency because of an inscrutable compiler error; that turned out to have been caused by it doing a web search and finding outdated documentation. It also likes to do LLM things ("I left that legacy function in there because it might be useful later on") once in a while.

    Still, I'll probably continue to use GenAI for programming. Programming is fun, but after doing that for 20+ years, I'm starting to feel that getting results fast is even more fun than just coding. Using the agent is an enormous speed-up over doing everything myself, especially writing tests or debugging compiler errors -- but at least for now, I still abhor the idea of vibe coding and truly leaving everything up to the AI agent.

    The final downside of the whole GenAI programming shtick I noticed, is that sometimes you become so preoccupied with whether you can write some program quickly, that you don't stop to think whether you should write that program at all...

    (Edit: Removed a stray sentence that I forgot to cut out)

    7 votes
  11. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link Parent
    That particular horse has left the barn at least twenty years ago, when GMail decided to rely on search instead of organizing emails into folders. Sometimes it is a better trade-off to spend some...

    That particular horse has left the barn at least twenty years ago, when GMail decided to rely on search instead of organizing emails into folders. Sometimes it is a better trade-off to spend some time searching for something rather than organizing everything.

    I think I get what you mean though. People -- and I definitively include myself here -- will inevitably start trusting the automation (since it works almost all of the time) to a degree that is unwarranted, and then they won't want to or even be able to complete the task on their own.

    1 vote
  12. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link Parent
    I've built a few smaller tools, like one that renames scientific papers to a readable title instead of the <opaque numbers>.pdf you usually get from downloading them. The first page is run through...

    I've built a few smaller tools, like one that renames scientific papers to a readable title instead of the <opaque numbers>.pdf you usually get from downloading them. The first page is run through pdfttotext and the LLM extracts the title as the new filename. Simple, but useful -- and almost impossible to do right with just RegEx.

    I've also used local LLMs for extracting JSON from raw OCR data (you can instruct, and even force, a model to output JSON only). That worked reasonably well, it even caught a few cases where the ground-truth labels I had were wrong. And since the process was automated, I threw multiple different models at it and then looked at examples where the LLMs disagreed.

    More about LLama.cpp

    LLama.cpp is basically a toolkit for running LLMs and Vision-Language Models -- it can be compiled from scratch, but there are also pre-built packages available, e.g. in Homebrew on Mac and Linux.

    If you're on Windows, KoboldCPP, which is a llama.cpp-fork that is geared towards role-playing and creative writing, has the easier setup -- there's a .exe-file to download which comes with a graphical wizard for configuration.

    The software provides an API endpoint that is (partly) compatible with the OpenAI API so that you can use, for example, the openai package from PyPI to interact with it once you start the server. KoboldCPP listens on http://localhost:5001/v1/ for API requests by default, and llama.cpp comes with a llama-server executable that has a webinterface and a similar OpenAI-compatible endpoint. Switching a program from OpenAI or OpenRouter to a local server or vice versa generally involves just a two-line change (setting the API base-url and adding or removing the api-key).

    That said, since you're using Ollama already, the easiest path would probably be to just set the context to a value that's larger than 2048 (if that is indeed the problem). I think you need to create a new modelfile for that, but it's been a while since I played with Ollama.

    2 votes
  13. Comment on What programming/technical projects have you been working on? in ~comp

    DataWraith
    Link Parent
    Ollama defaults to a context size of 2048 tokens, and feeding in a single document, let alone multiple, can very quickly blow through that budget. If your system prompt comes before you feed in...

    Ollama defaults to a context size of 2048 tokens, and feeding in a single document, let alone multiple, can very quickly blow through that budget. If your system prompt comes before you feed in the documents, it may fall out of the context window entirely -- then the model reverts to just trying to make up plausible continuations because it can't see the instructions. This is a common pain point with Ollama and might explain the poor performance you're seeing.

    I've been bitten by this myself and have avoided Ollama since then. You can configure it to use a larger context size, but since it is just a thin-ish wrapper around llama.cpp, it doesn't add all that much value over using llama.cpp directly IMO.

    4 votes
  14. Comment on What are the best truly unbeatable E2EE, presumably P2P messaging apps? in ~tech

    DataWraith
    Link
    There's really no way to prevent an app from stealing content after it has been decrypted -- it has to connect to the internet to be useful, and you realistically won't be able to audit every...

    There's really no way to prevent an app from stealing content after it has been decrypted -- it has to connect to the internet to be useful, and you realistically won't be able to audit every single connection it makes.

    Having a peer-to-peer architecture won't really change that (in fact, it makes auditing all connections even harder because you connect to random strangers), and additionally exposes your IP-address to random other peers in the network.

    That said, you might want to look at Simplex.chat. It has a number of favorable properties -- its architecture sits somewhere between a peer-to-peer network and centralized servers. You connect to one of a set of relay servers (that you can self-host too, if you want), and the recipient has to fetch their messages from that same relay server. Channel identities are ephemeral and everything is heavily encrypted to make it very hard to link sender and recipients even if you control the relay server that is being used, though it is still not impossible IIUC.

    16 votes
  15. Comment on Day 23: LAN Party in ~comp.advent_of_code

    DataWraith
    Link
    This one was a lot of fun. I guess I'm much more relaxed when I already failed my goal... I struggled a bit at first, trying to re-use the part 1 implementation for part 2, but that didn't work,...

    This one was a lot of fun. I guess I'm much more relaxed when I already failed my goal...

    I struggled a bit at first, trying to re-use the part 1 implementation for part 2, but that didn't work, likely because I made mistakes with updating the graph connectivity when merging nodes, but in the end, I did find a solution that runs acceptably fast.

    Parser

    This uses the petgraph library. I found it somewhat hard to use in the past, but it worked out nicely here.

    pub fn parser(input: &'static str) -> PuzzleInput {
        let mut graph = UnGraphMap::new();
    
        for line in input.lines() {
            let (a, b) = line.split_once("-").unwrap();
            graph.add_edge(a, b, ());
        }
    
        PuzzleInput { graph }
    }
    
    Part 1 (Rust)
    pub fn part1(input: &PuzzleInput) -> String {
        // This is a bit of a mouthful, but petgraph makes this surprisingly easy.
        //
        // 1. Iterate over all nodes that start with "t"
        // 2. Get all neighbors of the node
        // 3. Check all combinations of two neighbors if they are connected -- if so, we have a three-connected set
        // 4. Sort the three-connected set and return it as a vector
        // 5. At this point, we have a vector of vectors for each node that starts with "t",
        //    so we need to flatten it to remove one level of nesting
        // 6. Then we remove duplicates using .unique() and return the length of the result
        input
            .graph
            .nodes()
            .filter(|n| n.starts_with("t"))
            .map(|n| {
                input
                    .graph
                    .neighbors(n)
                    .tuple_combinations()
                    .filter(|(a, b)| input.graph.contains_edge(a, b))
                    .map(|(a, b)| {
                        let mut triplet = vec![n, a, b];
                        triplet.sort();
                        triplet
                    })
                    .collect_vec()
            })
            .flatten()
            .unique()
            .count()
            .to_string()
    }
    
    Part 2
    pub fn part2(input: &PuzzleInput) -> String {
        let mut visited = HashSet::new();
        let mut best_clique = Vec::new();
    
        // This is pretty much a brute-force approach that follows directly from the
        // definition of a clique.
        //
        // We start with a single-node clique and try to grow it. To do that, we
        // iterate over all nodes that have not been processed yet and check if they
        // are connected to all nodes in the current clique.
        //
        // If they are connected to everyone in the clique, they are part of the
        // clique by definition, so we add them to the set and continue.
        //
        // Once a node is part of a clique, we add it to the visited set to avoid
        // processing it again -- it can't be part of another, larger clique: either
        // the current clique grows to be the largest possible, or another, larger
        // clique is found which will not contain the node because it is part of
        // the current clique.
        //
        // Not 100% sure if this is just fast because the input is benign, or if it's
        // actually a sound algorithm...
        for start in input.graph.nodes() {
            if visited.contains(start) {
                continue;
            }
    
            let mut clique = Vec::from([start]);
    
            'outer: for node in input.graph.neighbors(start) {
                for c in clique.iter() {
                    if !input.graph.contains_edge(c, node) {
                        continue 'outer;
                    }
                }
    
                clique.push(node);
                visited.insert(node);
            }
    
            if clique.len() > best_clique.len() {
                best_clique = clique;
            }
        }
    
        best_clique.iter().sorted().join(",")
    }
    
    Benchmark
    day_23    fastest       │ slowest       │ median        │ mean          │ samples │ iters
    ├─ part1  1.167 ms      │ 1.548 ms      │ 1.225 ms      │ 1.22 ms       │ 100     │ 100
    ╰─ part2  1.184 ms      │ 1.229 ms      │ 1.201 ms      │ 1.201 ms      │ 100     │ 100
    
    1 vote
  16. Comment on Day 21: Keypad Conundrum in ~comp.advent_of_code

    DataWraith
    Link
    This puzzle broke my brain, and not in a good way. :( After spending over 10 hours on it and only solving part 1, I gave up and looked up the solution. This is especially galling -- I really...

    This puzzle broke my brain, and not in a good way. :(

    After spending over 10 hours on it and only solving part 1, I gave up and looked up the solution. This is especially galling -- I really wanted to solve one AoC on my own, without needing help, which didn't work out last year either. So I guess I can try again next year. Really not feeling motivated to complete the calendar at all, tbh.

    And of course the solution turned out to be relatively simple again and should have been solveable with the tools I had at my disposal if I just had had a small bit of insight that was missing

    Spoiler

    -- that you can use pairs of characters instead of going character-by-character or A-button-by-A-button.

    It feels really bad to vent and not at least leave a solution here, so here's what I finally wrote after having the explanation spoon-fed to me. :(
    Solution, I guess
    pub fn part1(input: &PuzzleInput) -> String {
        let codepad = CodePad::new_codepad();
        let dirpad = CodePad::new_dirpad();
    
        let mut sum = 0;
    
        for code in input.codes.iter() {
            let solution = solve(code, &codepad, &dirpad, 2);
    
            let num = parse_uints(code)[0];
            sum += num as usize * solution;
        }
    
        sum.to_string()
    }
    
    // https://www.youtube.com/watch?v=dqzAaj589cM
    pub fn solve(code: &str, codepad: &CodePad, dirpad: &CodePad, depth: usize) -> usize {
        let inputs = codepad.solve(code);
    
        let mut best_length = usize::MAX;
    
        for seq in inputs.iter() {
            let mut length = 0;
    
            for (a, b) in std::iter::once('A').chain(seq.chars()).tuple_windows() {
                length += compute_length(dirpad, (a, b), depth);
            }
    
            best_length = best_length.min(length);
        }
    
        best_length
    }
    
    #[cached(key = "(char, char, usize)", convert = "{ (pair.0, pair.1, depth) }")]
    fn compute_length(dirpad: &CodePad, pair: (char, char), depth: usize) -> usize {
        if depth == 1 {
            return dirpad.sequences.get(&pair).unwrap()[0].len();
        }
    
        let mut optimal = usize::MAX;
    
        for seq in dirpad.sequences.get(&pair).unwrap().iter() {
            let mut length = 0;
    
            for (a, b) in std::iter::once('A').chain(seq.chars()).tuple_windows() {
                length += compute_length(dirpad, (a, b), depth - 1);
            }
    
            optimal = optimal.min(length);
        }
    
        optimal
    }
    
    #[derive(Clone, Debug)]
    pub struct CodePad {
        pub pad: Grid2D<char>,
        pub positions: BTreeMap<char, Coordinate>,
        pub sequences: HashMap<(char, char), Vec<String>>,
    }
    
    impl CodePad {
        pub fn new_codepad() -> Self {
            let grid = Grid2D::new(3, 4, '.');
    
            let mut codepad = Self {
                pad: grid,
                positions: BTreeMap::new(),
                sequences: HashMap::new(),
            };
    
            codepad.set((0, 0).into(), '7');
            codepad.set((1, 0).into(), '8');
            codepad.set((2, 0).into(), '9');
            codepad.set((0, 1).into(), '4');
            codepad.set((1, 1).into(), '5');
            codepad.set((2, 1).into(), '6');
            codepad.set((0, 2).into(), '1');
            codepad.set((1, 2).into(), '2');
            codepad.set((2, 2).into(), '3');
            codepad.set((1, 3).into(), '0');
            codepad.set((2, 3).into(), 'A');
    
            codepad.precalculate_movement_sequence();
    
            codepad
        }
    
        pub fn new_dirpad() -> Self {
            let grid = Grid2D::new(3, 2, '.');
    
            let mut dir_pad = Self {
                pad: grid,
                positions: BTreeMap::new(),
                sequences: HashMap::new(),
            };
    
            dir_pad.set((1, 0).into(), '^');
            dir_pad.set((2, 0).into(), 'A');
            dir_pad.set((0, 1).into(), '<');
            dir_pad.set((1, 1).into(), 'v');
            dir_pad.set((2, 1).into(), '>');
    
            dir_pad.precalculate_movement_sequence();
    
            dir_pad
        }
    
        pub fn solve(&self, input: &str) -> Vec<String> {
            let mut options = Vec::new();
    
            // We start with all robots pointing at the A button, so we need to add that to the input --
            // otherwise we'll have no way to press A at the start.
            let input = std::iter::once('A').chain(input.chars());
    
            for (a, b) in input.tuple_windows() {
                let seq = self.sequences.get(&(a, b)).unwrap_or_else(|| {
                    panic!("No sequence found for {:?}", (a, b));
                });
    
                options.push(seq);
            }
    
            let mut result = Vec::new();
    
            for prod in options.iter().map(|v| v.iter()).multi_cartesian_product() {
                let path = prod.into_iter().join("");
                result.push(path);
            }
    
            result
        }
    
        pub fn precalculate_movement_sequence(&mut self) {
            for x in 0..self.pad.width() {
                for y in 0..self.pad.height() {
                    for x2 in 0..self.pad.width() {
                        for y2 in 0..self.pad.height() {
                            let position = Coordinate::new(x as i32, y as i32);
                            let position2 = Coordinate::new(x2 as i32, y2 as i32);
    
                            if !self.is_valid_position(position) || !self.is_valid_position(position2) {
                                continue;
                            }
    
                            let c1 = self.pad.get(position).unwrap();
                            let c2 = self.pad.get(position2).unwrap();
    
                            if c1 == c2 {
                                self.sequences.insert((*c1, *c2), vec!["A".to_string()]);
                                continue;
                            }
    
                            // BFS to find the shortest path
                            let mut q = VecDeque::new();
                            q.push_back((position, String::new()));
                            let mut best_len = usize::MAX;
    
                            while let Some((current, path)) = q.pop_front() {
                                if current == position2 {
                                    best_len = path.len();
    
                                    let mut path = path.clone();
                                    path.push('A');
    
                                    self.sequences
                                        .entry((*c1, *c2))
                                        .or_default()
                                        .push(path);
                                    continue;
                                }
    
                                if path.len() > best_len {
                                    break;
                                }
    
                                for d in Direction::cardinal() {
                                    let next = current.neighbor(d);
                                    let mut next_path = path.clone();
    
                                    match d {
                                        Direction::Up => next_path.push('^'),
                                        Direction::Down => next_path.push('v'),
                                        Direction::Left => next_path.push('<'),
                                        Direction::Right => next_path.push('>'),
                                        _ => unreachable!(),
                                    }
    
                                    if self.is_valid_position(next) {
                                        q.push_back((next, next_path));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    
        fn is_valid_position(&self, position: Coordinate) -> bool {
            self.pad.get(position).is_some() && self.pad.get(position).unwrap() != &'.'
        }
    
        pub fn set(&mut self, position: Coordinate, value: char) {
            self.pad.set(position, value);
            self.positions.insert(value, position);
        }
    }
    
    1 vote
  17. Comment on Day 20: Race Condition in ~comp.advent_of_code

    DataWraith
    Link
    Holy off-by-one errors, Batman! Part 1 (Rust) pub fn part1(input: &PuzzleInput) -> String { let path_grid = shortest_path_grid(&input.maze); let cheats = find_cheats(&path_grid); let mut result =...

    Holy off-by-one errors, Batman!

    Part 1 (Rust)
    pub fn part1(input: &PuzzleInput) -> String {
        let path_grid = shortest_path_grid(&input.maze);
        let cheats = find_cheats(&path_grid);
    
        let mut result = 0;
        for cheat in cheats.iter() {
            if cheat.2 >= 100 {
                result += 1;
            }
        }
    
        result.to_string()
    }
    
    pub fn find_cheats(grid: &Grid2D<u32>) -> Vec<(Coordinate, Direction, u32)> {
        let mut result = Vec::new();
    
        for x in 1..(grid.width() - 1) {
            for y in 1..(grid.height() - 1) {
                for dir in Direction::cardinal() {
                    let pos = Coordinate::new(x as i32, y as i32);
    
                    if grid[pos] == u32::MAX {
                        continue;
                    }
    
                    let neighbor = pos.neighbor(dir);
                    let neighbor_neighbor = neighbor.neighbor(dir);
    
                    if grid[neighbor] != u32::MAX {
                        continue;
                    }
    
                    if grid.get(neighbor_neighbor).is_none() {
                        continue;
                    }
    
                    if grid[neighbor_neighbor] == u32::MAX {
                        continue;
                    }
    
                    let cur_val = grid[pos];
                    let next_val = grid[neighbor_neighbor];
    
                    if (cur_val.saturating_sub(next_val).saturating_sub(2) > 0) {
                        result.push((pos, dir, (cur_val - next_val).saturating_sub(2)));
                    }
                }
            }
        }
    
        result
    }
    
    pub fn shortest_path_grid(grid: &Grid2D<char>) -> Grid2D<u32> {
        let mut result = grid.map(|_| u32::MAX);
        let end = grid.iter().find(|(_, &c)| c == 'E').unwrap().0;
    
        let mut queue = VecDeque::new();
        queue.push_back((end, 0));
    
        while let Some((current, distance)) = queue.pop_front() {
            result[current] = distance;
    
            if grid[current] == 'S' {
                return result;
            }
    
            for neighbor in current.neighbors() {
                if grid[neighbor] != '#' && result[neighbor] > distance + 1 {
                    queue.push_back((neighbor, distance + 1));
                }
            }
        }
    
        unreachable!()
    }
    
    Part 2 (Rust)

    This runs for almost 3 seconds, I'll need to improve that later.

    pub fn part2(input: &PuzzleInput) -> String {
        let path_grid = shortest_path_grid(&input.maze);
        let mut cheat_collection: HashMap<u32, HashSet<(Coordinate, Coordinate)>> = HashMap::new();
    
        for x in 1..(path_grid.width() - 1) {
            for y in 1..(path_grid.height() - 1) {
                let pos = Coordinate::new(x as i32, y as i32);
    
                let cheats = find_cheats(&path_grid, pos);
                for (dist, pairs) in cheats.iter() {
                    cheat_collection.entry(*dist).or_default().extend(pairs);
                }
            }
        }
    
        let mut result = 0;
    
        for (dist, pairs) in cheat_collection.iter() {
            if dist >= &100 {
                result += pairs.len();
            }
        }
    
        result.to_string()
    }
    
    fn find_cheats(
        grid: &Grid2D<u32>,
        start_pos: Coordinate,
    ) -> HashMap<u32, HashSet<(Coordinate, Coordinate)>> {
        if grid[start_pos] == u32::MAX {
            return HashMap::new();
        }
    
        let mut q = VecDeque::new();
        q.push_back((start_pos, 0u32));
    
        let mut result: HashMap<u32, HashSet<(Coordinate, Coordinate)>> = HashMap::new();
        let mut visited = HashSet::new();
    
        while let Some((pos, dist)) = q.pop_front() {
            if dist >= 20 {
                break;
            }
    
            if !visited.insert((pos, dist)) {
                continue;
            }
    
            for dir in Direction::cardinal() {
                let neighbor = pos.neighbor(dir);
    
                if grid.get(neighbor).is_none() {
                    continue;
                }
    
                if grid[neighbor] != u32::MAX {
                    let saved = grid[start_pos]
                        .saturating_sub(grid[neighbor])
                        .saturating_sub(start_pos.manhattan_distance(neighbor) as u32);
    
                    if saved > 0 {
                        result
                            .entry(saved)
                            .or_default()
                            .insert((start_pos, neighbor));
                    }
                }
    
                q.push_back((neighbor, dist + 1));
            }
        }
    
        result
    }
    
    4 votes
  18. Comment on Day 19: Linen Layout in ~comp.advent_of_code

    DataWraith
    Link
    Very nice and relaxing puzzle. Part 1 (Rust) pub fn part1(input: &PuzzleInput) -> String { possible_patterns(input).len().to_string() } pub fn possible_patterns(input: &PuzzleInput) ->...

    Very nice and relaxing puzzle.

    Part 1 (Rust)
    pub fn part1(input: &PuzzleInput) -> String {
        possible_patterns(input).len().to_string()
    }
    
    pub fn possible_patterns(input: &PuzzleInput) -> HashSet<String> {
        let mut q = BTreeSet::new();
        let mut c = HashSet::new();
    
        for (i, design) in input.desired_designs.iter().enumerate() {
            q.insert((i, design.as_str()));
        }
    
        while let Some(design) = q.pop_first() {
            for pattern in input.patterns.iter() {
                if design.1.starts_with(pattern) {
                    if design.1.len() == pattern.len() {
                        c.insert(input.desired_designs[design.0].clone());
                        q.retain(|d| d.0 != design.0);
                    } else {
                        q.insert((design.0, &design.1[pattern.len()..]));
                    }
                }
            }
        }
    
        c
    }
    
    Part 2 (Rust)
    pub fn part2(input: &PuzzleInput) -> String {
        let possible = possible_patterns(input);
    
        let mut c = 0;
    
        for design in possible.iter() {
            c += count_possibilities(input, design);
        }
    
        c.to_string()
    }
    
    pub fn count_possibilities(input: &PuzzleInput, design: &str) -> usize {
        let mut c = 0;
    
        let mut q = BTreeMap::new();
        q.insert(design, 1);
    
        while let Some((design, paths)) = q.pop_first() {
            if design.len() == 0 {
                c += paths;
                continue;
            }
    
            for pattern in input.patterns.iter() {
                if design.starts_with(pattern) {
                    q.entry(&design[pattern.len()..])
                        .and_modify(|p| *p += paths)
                        .or_insert(paths);
                }
            }
        }
    
        c
    }
    
    2 votes
  19. Comment on Day 18: RAM Run in ~comp.advent_of_code

    DataWraith
    (edited )
    Link
    Oh my god, somehow I fall for every single trap and/or nerd-snipe in AoC this year... Rust (Part 1) A straightforward Breadth-First Search to find the path. pub fn part1(input: &PuzzleInput) ->...

    Oh my god, somehow I fall for every single trap and/or nerd-snipe in AoC this year...

    Rust (Part 1)

    A straightforward Breadth-First Search to find the path.

    pub fn part1(input: &PuzzleInput) -> String {
        let mut memory = Grid2D::new(input.width, input.height, '.');
    
        // Mark the fallen bytes
        for c in input.bytes.iter().take(input.to_simulate) {
            memory.set(*c, '#');
        }
    
        // And find the shortest path to the bottom right using Breadth-First Search
        let mut q = VecDeque::new();
        q.push_back((Coordinate::new(0, 0), 0));
    
        while let Some((c, cost)) = q.pop_front() {
            if c == Coordinate::new(input.width as i32 - 1, input.height as i32 - 1) {
                return cost.to_string();
            }
    
            for nc in c.neighbors() {
                if memory.get(nc) == Some(&'.') {
                    memory.set(nc, 'o');
                    q.push_back((nc, cost + 1));
                }
            }
        }
    
        unreachable!()
    }
    
    Part 2

    That's where the trap hit. I tried to compute the set of all valid paths as a Dijkstra-Map, and then tried to repair it when an obstacle appears. I'm sure this is possible somehow (there are decremental-connectivity data structures, after all), but it is entirely unnecessary for this problem. You just A* repeatedly whenever a new obstacle falls onto the previous best path...

    pub fn part2(input: &PuzzleInput) -> String {
        let mut memory = Grid2D::new(input.width, input.height, false);
        let mut path: Option<HashSet<Coordinate>> = None;
    
        // Simulate the falling bytes
        for byte_coord in input.bytes.iter() {
            // If the byte falls on our exit, we're trapped!
            if byte_coord.x == input.width as i32 - 1 && byte_coord.y == input.height as i32 - 1 {
                return format!("{},{}", byte_coord.x, byte_coord.y);
            }
    
            // Mark the byte as fallen
            memory.set(*byte_coord, true);
    
            // If we don't have a valid path (either because this is the first invocation, or because a byte fell onto our path),
            // find the shortest path to the bottom right using A*
            if path.is_none() || path.clone().unwrap().contains(byte_coord) {
                let start = Coordinate::new(0, 0);
                let end = Coordinate::new(input.width as i32 - 1, input.height as i32 - 1);
                let mut visited = memory.map(|_| false);
    
                // This is a bit ugly, but it appeases the borrow checker
                let m2 = memory.clone();
    
                // Define the successors function that returns the valid neighbors of the current coordinate
                let successors = move |c: &Coordinate| {
                    if *c == end {
                        return vec![];
                    }
    
                    let neighbors = c
                        .neighbors()
                        .filter(|n| m2.get(*n).is_some())
                        .filter(|n| *m2.get(*n).unwrap() == false)
                        .filter(|n| *visited.get(*n).unwrap() == false)
                        .map(|n| (n, 1))
                        .collect_vec();
    
                    // Mark the neighbors as visited
                    neighbors.iter().for_each(|n| {
                        visited.set(n.0, true);
                    });
    
                    neighbors
                };
    
                // And then just A* and convert the path into a set of coordinates
                path = astar(
                    &start,
                    successors,
                    |c| *c == end,
                    |c| c.manhattan_distance(end),
                )
                .map(|(path, cost)| path.iter().cloned().collect::<HashSet<_>>());
            }
    
            // If we don't have a valid path, the last byte that fell is our puzzle answer.
            if path.is_none() {
                return format!("{},{}", byte_coord.x, byte_coord.y);
            }
        }
    
        unreachable!()
    }
    

    The astar function is from my AoC helper library, but you could just as well use a different implementation (e.g. the pathfinding crate's).

    Non-stupid way to do Part 2
    pub fn part2(input: &PuzzleInput) -> String {
        // This solution uses UnionFind again. I thought of trying this myself, but
        // concluded that it wouldn't work -- and it doesn't in the forward
        // direction. But I later found out on Reddit that you can just reverse
        // everything, and it works marvelously.
    
        // Step 1: Find all obstacle coordinates and make a map
        let mut grid = Grid2D::new(input.width, input.height, '.');
    
        for b in input.bytes.iter() {
            grid.set(*b, '#');
        }
    
        // Step 2: Prepare the UnionFind datastructure
        let mut union_find = UnionFind::default();
        let mut sets = grid.map(|_| 0);
    
        for (c, _) in grid.iter() {
            sets[c] = union_find.make_set();
        }
    
        // Step 3: Union all coordinates that never have obstacles
        for (c, _) in grid.iter() {
            if grid.get(c) != Some(&'.') {
                continue;
            }
    
            for dir in [Direction::Right, Direction::Down] {
                let n = c.neighbor(dir);
    
                if grid.get(n) == Some(&'.') {
                    union_find.union(sets[c], sets[n]).unwrap();
                }
            }
        }
    
        // Step 4: Iterate over the obstacles in reverse, and union the coordinates.
        // This is the non-obvious part. Instead of adding obstacles one by one,
        // we start with all obstacles and then remove them one by one, unioning
        // the free space.
        let start = Coordinate::new(0, 0);
        let end = Coordinate::new(input.width as i32 - 1, input.height as i32 - 1);
    
        for b in input.bytes.iter().rev() {
            grid.set(*b, '.');
    
            for n in b.neighbors() {
                if grid.get(n) == Some(&'.') {
                    union_find.union(sets[*b], sets[n]).unwrap();
                }
            }
    
            // Step 5: If the start and end coordinates are connected, the last
            // obstacle we removed must have been the one blocking the path.
            if union_find.find(sets[start]) == union_find.find(sets[end]) {
                return format!("{},{}", b.x, b.y);
            }
        }
    
        unreachable!()
    }
    

    This runs about twice as fast on my machine as using a binary search, 150µs vs. ~300µs.