whispersilk's recent activity

  1. Comment on Day 6: Wait For It in ~comp.advent_of_code

    whispersilk
    Link
    Well, after day 5 this was refreshingly easy. Brute-force ran in ~4 ms even for part 2, but I wound up implementing a binary search that skips to linear searching when <10 elements are left...

    Well, after day 5 this was refreshingly easy. Brute-force ran in ~4 ms even for part 2, but I wound up implementing a binary search that skips to linear searching when <10 elements are left anyway. With that, runtime for parts 1 and 2 combined comes out around 12 μs, making today the fastest day by roughly an order of magnitude.

    Parser and data structure
    #[derive(Debug)]
    struct Race {
        time: usize,
        record: usize,
    }
    
    impl Race {
        fn num_solutions(&self) -> usize {
            let min = binary_search(0, self.time / 2 - 1, |val| {
                match (self.is_solution(val), self.is_solution(val + 1)) {
                    (true, true) => Ordering::Greater,
                    (false, false) => Ordering::Less,
                    (false, true) => Ordering::Equal,
                    (true, false) => unreachable!(),
                }
            });
            let max = binary_search(self.time / 2, self.time, |val| {
                match (self.is_solution(val), self.is_solution(val + 1)) {
                    (true, true) => Ordering::Less,
                    (false, false) => Ordering::Greater,
                    (true, false) => Ordering::Equal,
                    (false, true) => unreachable!(),
                }
            });
            let (max, min) = (max.expect("Max exists"), min.expect("Min exists"));
            max - min + 1
        }
    
        fn is_solution(&self, val: usize) -> bool {
            val * (self.time - val) > self.record
        }
    }
    
    impl From<(usize, usize)> for Race {
        fn from(val: (usize, usize)) -> Self {
            Self {
                time: val.0,
                record: val.1,
            }
        }
    }
    
    #[inline]
    fn binary_search<F>(mut bottom: usize, mut top: usize, cond: F) -> Option<usize>
    where
        F: Fn(usize) -> Ordering,
    {
        while bottom < top {
            if top - bottom < 10 {
                return (bottom..=top).find(|i| cond(*i) == Ordering::Equal);
            } else {
                let mid = (top + bottom) / 2;
                match cond(mid) {
                    Ordering::Less => bottom = mid - 1,
                    Ordering::Greater => top = mid + 1,
                    Ordering::Equal => return Some(mid),
                }
            }
        }
        None
    }
    
    fn parse_input(part: Puzzle) -> Result<Vec<Race>> {
        let input = load_input(6)?;
        let mut lines = input.lines();
        let times = lines
            .next()
            .and_then(|l| l.strip_prefix("Time:"))
            .ok_or("No line 1")?;
        let records = lines
            .next()
            .and_then(|l| l.strip_prefix("Distance:"))
            .ok_or("No line 2")?;
        match part {
            Puzzle::First => {
                let times = times
                    .split_whitespace()
                    .filter(|n| !n.is_empty())
                    .map(parse_as::<usize>)
                    .collect::<Result<Vec<usize>>>()?;
                let records = records
                    .split_whitespace()
                    .filter(|n| !n.is_empty())
                    .map(parse_as::<usize>)
                    .collect::<Result<Vec<usize>>>()?;
                Ok(times.into_iter().zip(records).map(Race::from).collect())
            }
            Puzzle::Second => {
                let time = parse_as::<usize>(&times.replace(' ', ""))?;
                let record = parse_as::<usize>(&records.replace(' ', ""))?;
                Ok(vec![(time, record).into()])
            }
        }
    }
    
    Part 1
    fn first() -> Result<String> {
        let races = parse_input(Puzzle::First)?;
        let total = races
            .into_iter()
            .map(|race| race.num_solutions())
            .product::<usize>()
            .to_string();
        Ok(total)
    }
    
    Part 2
    fn second() -> Result<String> {
        let race = parse_input(Puzzle::Second)?.remove(0);
        Ok(race.num_solutions().to_string())
    }
    
    1 vote
  2. Comment on Day 5: If You Give A Seed A Fertilizer in ~comp.advent_of_code

    whispersilk
    Link
    Rust At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a...

    Rust

    At first I did things the brute-force way, which was fine for part 1 but resulted in part 2 taking 85 seconds(!) to run — given that it was doing calculations for some 1.6 billion seeds on a single core, that's respectable, but I want things to run quickly. With some imports I could have made it multi-core, but I'm still sticking to std and that wouldn't have gotten it down to where I wanted anyway.

    In the end, though, I figured out the same technique of collapsing the layers into one so that the actual comparisons can be done both intelligently and all at once that it seems most people used. Getting all the edge cases nailed out was rough, but I got it and in the end both parts combined run in just under 200 μs.

    Of course, between implementing the smart solution and just not having much time the past few days I'm now a bit behind on the other days, so I have some catching up to do.

    Parser and data structure — sorry, this one's ugly
    #[derive(Debug, Clone, Copy)]
    struct Range {
        input_start: usize,
        input_end: usize,
        output_start: usize,
        output_end: usize,
    }
    
    enum CombinedRange {
        Input(Range),
        Output(Range),
        Overlap(Range),
        None,
    }
    
    impl Range {
        fn new(input_start: usize, input_end: usize, output_start: usize, output_end: usize) -> Self {
            Self { input_start, input_end, output_start, output_end }
        }
    
    
        fn translate(&self, val: usize) -> Option<usize> {
            if val >= self.input_start && val <= self.input_end {
                if self.input_start > self.output_start {
                    Some(val - (self.input_start - self.output_start))
                } else {
                    Some(val + (self.output_start - self.input_start))
                }
            } else {
                None
            }
        }
    
        fn translate_min(&self, range: (usize, usize)) -> Option<usize> {
            if range.1 < self.input_start || range.0 > self.input_end {
                None
            } else {
                self.translate(std::cmp::max(range.0, self.input_start))
            }
        }
    
        fn overlaps(input: &Range, output: &Range) -> bool {
            input.output_end >= output.input_start && input.output_start <= output.input_end
        }
    
        fn input_for(&self, output: usize) -> usize {
            let offset = output - self.output_start;
            self.input_start + offset
        }
    
        fn output_for(&self, input: usize) -> usize {
            let offset = input - self.input_start;
            self.output_start + offset
        }
    
        // Splits an input range based on a value in its output range.
        // The provided value is part of the SECOND range returned, if applicable.
        fn split_at_output(self, second_start: usize) -> (Option<Range>, Option<Range>) {
            if second_start <= self.output_start {
                (None, Some(self))
            } else if second_start > self.output_end {
                (Some(self), None)
            } else {
                let first_len = second_start - 1 - self.output_start;
                let first = Range::new(self.input_start, self.input_start + first_len, self.output_start, self.output_start + first_len);
                let second = Range::new(first.input_end + 1, self.input_end, first.output_end + 1, self.output_end);
                (Some(first), Some(second))
            }
        }
    
        fn combine(input: Range, output: Range) -> [CombinedRange; 3] {
            use CombinedRange::*;
            use std::cmp::Ordering;
            if !Range::overlaps(&input, &output) {
                [Input(input), Output(output), None]
            } else {
                let (low, high) = (
                    std::cmp::max(input.output_start, output.input_start),
                    std::cmp::min(input.output_end, output.input_end),
                );
                let overlap = Overlap(Range::new(input.input_for(low), input.input_for(high), output.output_for(low), output.output_for(high)));
                match input.output_start.cmp(&output.input_start) {
                    Ordering::Less => {
                        let first = Input(Range::new(input.input_start, input.input_for(low - 1), input.output_start, low - 1));
                        match input.output_end.cmp(&output.input_end) {
                            Ordering::Less => [
                                first,
                                overlap,
                                Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            ],
                            Ordering::Equal => [
                                first,
                                overlap,
                                None,
                            ],
                            Ordering::Greater => [
                                first,
                                overlap,
                                Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            ],
                        }
                    }
                    Ordering::Equal => match input.output_end.cmp(&output.input_end) {
                        Ordering::Less => [
                            overlap,
                            Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            None,
                        ],
                        Ordering::Equal => [
                            overlap,
                            None,
                            None,
                        ],
                        Ordering::Greater => [
                            overlap,
                            Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            None,
                        ],
                    }
                    Ordering::Greater => {
                        let first = Output(Range::new(output.input_start, low - 1, output.output_start, output.output_for(low - 1)));
                        match input.output_end.cmp(&output.input_end) {
                            Ordering::Less => [
                                first,
                                overlap,
                                Output(Range::new(high + 1, output.input_end, output.output_for(high + 1), output.output_end)),
                            ],
                            Ordering::Equal => [
                                first,
                                overlap,
                                None,
                            ],
                            Ordering::Greater => [
                                first,
                                overlap,
                                Input(Range::new(input.input_for(high + 1), input.input_end, high + 1, input.output_end)),
                            ],
                        }
                    }
                }
            }
        }
    }
    
    fn parse_input() -> Result<(Vec<usize>, Vec<Range>)> {
        let input = load_input(5)?;
        let (seeds, maps) = input.split_once("\n\n").ok_or("No section divider")?;
        let seeds = seeds
            .strip_prefix("seeds: ")
            .ok_or("No seeds prefix")?
            .split(' ')
            .map(parse_as::<usize>)
            .collect::<Result<Vec<usize>>>()?;
        let map = maps
            .split("\n\n")
            .try_fold(Vec::new(), |prior, section| {
                let mut ranges = section
                    .split('\n')
                    .skip(1)
                    .filter(|line| !line.is_empty())
                    .map(|line| {
                        let nums = line
                            .split(' ')
                            .map(parse_as::<usize>)
                            .collect::<Result<Vec<usize>>>()?;
                        if let [dest, source, len] = nums.as_slice() {
                            Ok(Range::new(*source, source + len - 1, *dest, dest + len - 1))
                        } else {
                            Err(format!("Wrong number of elements on line {line}").into())
                        }
                    })
                    .collect::<Result<Vec<Range>>>()?;
                let mut new_ranges = Vec::new();
                for mut input in prior {
                    let mut outputs = ranges
                        .iter()
                        .enumerate()
                        .filter(|(_, output)| Range::overlaps(&input, output))
                        .map(|(idx, _)| idx)
                        .collect::<Vec<_>>();
                    outputs.sort_unstable();
                    let mut outputs = outputs
                        .into_iter()
                        .rev()
                        .map(|idx| ranges.remove(idx))
                        .collect::<Vec<_>>();
                    outputs.sort_unstable_by_key(|range| range.input_start);
                    if outputs.len() > 1 {
                        for output in outputs {
                            match input.split_at_output(output.input_end + 1) {
                                (Some(overlaps), Some(after)) => {
                                    input = after;
                                    for range in Range::combine(overlaps, output) {
                                        match range {
                                            CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                            CombinedRange::Output(x) => ranges.push(x),
                                            CombinedRange::None => (),
                                        }
                                    }
                                }
                                (Some(overlaps), None) => {
                                    for range in Range::combine(overlaps, output) {
                                        match range {
                                            CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                            CombinedRange::Output(x) => ranges.push(x),
                                            CombinedRange::None => (),
                                        }
                                    }
                                }
                                (None, Some(_)) => unreachable!(),
                                (None, None) => unreachable!(),
                            }
                        }
                    } else if !outputs.is_empty() {
                        let output = outputs[0];
                        for range in Range::combine(input, output) {
                            match range {
                                CombinedRange::Input(x) | CombinedRange::Overlap(x) => new_ranges.push(x),
                                CombinedRange::Output(x) => ranges.push(x),
                                CombinedRange::None => (),
                            }
                        }
                    } else {
                        new_ranges.push(input);
                    }
                }
                new_ranges.extend(ranges);
                let output: Result<Vec<_>> = Ok(new_ranges);
                output
            })?;
    
        Ok((seeds, map))
    }
    
    Part 1
    fn first() -> Result<String> {
        let (seeds, ranges) = parse_input()?;
        let min_seed_location = seeds
            .into_iter()
            .map(|seed| ranges.iter().find_map(|range| range.translate(seed)).unwrap_or(seed))
            .min()
            .ok_or("There is at least one seed")?
            .to_string();
        Ok(min_seed_location)
    }
    
    Part 2
    fn second() -> Result<String> {
        let (seeds, ranges) = parse_input()?;
        let mut seed_ranges = Vec::new();
        for x in (1..seeds.len()).step_by(2) {
            seed_ranges.push((seeds[x - 1], seeds[x - 1] + seeds[x] + 1));
        }
    
        let min_seed_location = seed_ranges
            .into_iter()
            .map(|seed_range| ranges.iter().find_map(|range| range.translate_min(seed_range)).unwrap_or(seed_range.0))
            .min()
            .ok_or("There is at least one seed range")?
            .to_string();
        Ok(min_seed_location)
    }
    
    1 vote
  3. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    whispersilk
    Link Parent
    I'm sticking to the standard library only for as long as that's tenable, so I'm just splitting on spaces etc and using str::parse<u32> to get numbers out. My code is linked down below. Having...

    I'm sticking to the standard library only for as long as that's tenable, so I'm just splitting on spaces etc and using str::parse<u32> to get numbers out. My code is linked down below.

    Having taken a look at your code now, I think the speed difference is probably coming from the fact that you're storing winning_numbers and numbers in BTreeSets and performing intersections as you go. I just calculated how many numbers on each card were winners at the time of parsing and stored that, so I never paid for node allocation or the cache misses involved in searching.

    2 votes
  4. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    whispersilk
    Link
    Rust Data structures and parser #[derive(Debug)] struct Card { winners: usize, } impl Card { fn num_winners(&self) -> usize { self.winners } fn score(&self) -> u32 { 1 << self.winners >> 1 } }...

    Rust

    Data structures and parser
    #[derive(Debug)]
    struct Card {
        winners: usize,
    }
    
    impl Card {
        fn num_winners(&self) -> usize {
            self.winners
        }
    
        fn score(&self) -> u32 {
            1 << self.winners >> 1
        }
    }
    
    impl TryFrom<&str> for Card {
        type Error = Box<dyn std::error::Error>;
        fn try_from(value: &str) -> Result<Self> {
            type Res<T> = std::result::Result<Vec<T>, std::num::ParseIntError>;
    
            let (_, data) = value.split_once(':').ok_or("Line is missing card id")?;
            let (cards, have) = data.split_once('|').ok_or("Line is missing '|'")?;
            let cards: Vec<_> = cards
                .trim()
                .split(' ')
                .filter(|s| s.len() > 0)
                .map(|n| n.trim().parse::<u32>())
                .collect::<Res<_>>()?;
            let winners = have
                .trim()
                .split(' ')
                .filter(|s| s.len() > 0)
                .map(|n| n.trim().parse::<u32>().map(|val| cards.iter().find(|v| **v == val)))
                .try_fold(0, |acc, x| x.map(|opt| acc + opt.map(|_| 1).unwrap_or(0)))?;
            Ok(Card { winners })
        }
    }
    
    Part 1
    fn first() -> Result<String> {
        let summed_scores = load_input(4)?
            .lines()
            .map(Card::try_from)
            .try_fold(0, |acc, x| x.map(|card| acc + card.score()))?
            .to_string();
        Ok(summed_scores)
    }
    
    Part 2
    fn second() -> Result<String> {
        let cards = load_input(4)?
            .lines()
            .map(Card::try_from)
            .collect::<Result<Vec<Card>>>()?;
        let num_cards = cards.len();
        let mut copies = vec![1; num_cards];
        for (idx, card) in cards.iter().enumerate() {
            let curr_count = copies[idx];
            let num_to_boost = card.num_winners();
            for i in (idx + 1)..=(std::cmp::min(idx + num_to_boost, num_cards)) {
                copies[i] += curr_count;
            }
        }
        let total_copies = copies.iter().sum::<u32>().to_string();
        Ok(total_copies)
    }
    

    What I've got so far is up on GitHub.

    1 vote
  5. Comment on Day 4: Scratchcards in ~comp.advent_of_code

    whispersilk
    Link Parent
    I haven't looked at Toric's code yet, so I don't know what we're doing differently in terms of parsing, but I'm using Rust too and with optimizations turned on, both parts together take 345 μs...

    I haven't looked at Toric's code yet, so I don't know what we're doing differently in terms of parsing, but I'm using Rust too and with optimizations turned on, both parts together take 345 μs including the time it takes to read the file from disk for each part. Without optimizations it's more like 3.1 ms.

    2 votes
  6. Comment on Day 3: Gear Ratios in ~comp.advent_of_code

    whispersilk
    (edited )
    Link
    Rust This was fun! A little bit harder than yesterday but easier than day 1, I think. My solution isn't exactly optimized — there's definitely some nested looping going on that would slow things...

    Rust

    This was fun! A little bit harder than yesterday but easier than day 1, I think. My solution isn't exactly optimized — there's definitely some nested looping going on that would slow things down on larger inputs — but it's pretty quick at this scale and the hard part is still just the grid parsing. Because of that, I'll put my parser in a block of its own and then the solutions will be their own little follow-on blocks.

    Data structures and parser
    #[derive(Debug)]
    struct Part {
        id: u32,
        row: u32,
        col_left: u32,
        col_right: u32,
    }
    
    #[derive(Debug)]
    struct Symbol {
        kind: char,
        row: u32,
        col: u32,
    }
    
    impl Part {
        fn new(id: u32, row: u32, col_left: u32, col_right: u32) -> Self {
            Self {
                id,
                row,
                col_left,
                col_right,
            }
        }
    
        fn adjacent_to(&self, other: &Symbol) -> bool {
            other.row >= self.row.saturating_sub(1)
                && other.row <= self.row + 1
                && other.col >= self.col_left.saturating_sub(1)
                && other.col <= self.col_right + 1
        }
    }
    
    fn parse_grid() -> Result<(Vec<Part>, Vec<Symbol>)> {
        let (mut row, mut col) = (0, 0);
        let (mut parts, mut symbols) = (Vec::new(), Vec::new());
        let mut curr_id = None;
        for c in load_input(3)?.chars() {
            match c {
                '\n' => {
                    if let Some(id) = curr_id.take() {
                        parts.push(Part::new(id, row, col - 1 - id.ilog10(), col - 1));
                    };
                    row += 1;
                    col = 0;
                }
                '.' => {
                    if let Some(id) = curr_id.take() {
                        parts.push(Part::new(id, row, col - 1 - id.ilog10(), col - 1));
                    };
                    col += 1;
                }
                '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' => {
                    let curr = c as u32 - 48;
                    curr_id = curr_id.map(|v| v * 10 + curr).or(Some(curr));
                    col += 1;
                }
                kind => {
                    if let Some(id) = curr_id.take() {
                        parts.push(Part::new(id, row, col - 1 - id.ilog10(), col - 1));
                    };
                    symbols.push(Symbol { kind, row, col });
                    col += 1;
                }
            };
        }
        parts.sort_unstable_by(|a, b| a.row.cmp(&b.row).then(a.col_left.cmp(&b.col_left)));
        symbols.sort_unstable_by(|a, b| a.row.cmp(&b.row).then(a.col.cmp(&b.col)));
        Ok((parts, symbols))
    }
    
    Part 1
    fn first() -> Result<String> {
        let (parts, symbols) = parse_grid()?;
        let sum = parts
            .iter()
            .filter_map(|part| {
                symbols
                    .iter()
                    .find(|s| part.adjacent_to(s))
                    .map(|_| part.id)
            })
            .sum::<u32>();
        Ok(sum.to_string())
    }
    
    Part 2
    fn second() -> Result<String> {
        let (parts, symbols) = parse_grid()?;
        let ratio = symbols
            .iter()
            .filter(|symbol| symbol.kind == '*')
            .filter_map(|symbol| {
                let mut iter = parts.iter().filter(|part| part.adjacent_to(symbol));
                match (iter.next(), iter.next(), iter.next()) {
                    (Some(part1), Some(part2), None) => Some(part1.id * part2.id),
                    _ => None,
                }
            })
            .sum::<u32>();
        Ok(ratio.to_string())
    }
    

    After completing yesterday's puzzles I put what I've got so far up on GitHub, and I'll do my best to keep it updated as the month goes on.

    2 votes
  7. Comment on Day 2: Cube Conundrum in ~comp.advent_of_code

    whispersilk
    Link
    Rust again this year, helped by working with it a lot in the past year. Like last year, I'm going to stick to the standard library for as long as I can. Hopefully I won't fall completely off of...

    Rust again this year, helped by working with it a lot in the past year. Like last year, I'm going to stick to the standard library for as long as I can. Hopefully I won't fall completely off of doing this like I did last year!

    Parts 1 and 2 at once because, as other have mentioned, the complexity today was entirely in the parsing.
    use std::convert::TryFrom;
    
    use crate::{load_input, Puzzle, Result};
    
    fn first() -> Result<String> {
        let id_sum = load_input(2)?
            .lines()
            .map(Game::try_from)
            .filter(|game| game.as_ref().is_ok_and(|v| v.valid_for(12, 13, 14)))
            .try_fold(0, |acc, x| x.map(|game| acc + game.id))?
            .to_string();
        Ok(id_sum)
    }
    
    fn second() -> Result<String> {
        let power_sum = load_input(2)?
            .lines()
            .map(Game::try_from)
            .try_fold(0, |acc, x| x.map(|game| acc + game.power()))?
            .to_string();
        Ok(power_sum)
    }
    
    #[derive(Debug)]
    struct Game {
        pub id: u32,
        pub red: u32,
        pub green: u32,
        pub blue: u32,
    }
    
    impl Game {
        fn valid_for(&self, r: u32, g: u32, b: u32) -> bool {
            self.red <= r && self.green <= g && self.blue <= b
        }
    
        fn power(&self) -> u32 {
            self.red * self.green * self.blue
        }
    }
    
    impl TryFrom<&str> for Game {
        type Error = Box<dyn std::error::Error>;
        fn try_from(value: &str) -> Result<Self> {
            use std::cmp::max;
    
            let (id, rounds) = value.split_once(':').ok_or(format!("No ':' in {value}"))?;
            let id = id
                .strip_prefix("Game ")
                .ok_or(format!("{id} doesn't start with 'Game '"))?
                .parse::<u32>()?;
    
            let (red, green, blue) = rounds
                .split(';')
                .map(|round| {
                    let (mut r, mut g, mut b) = (0, 0, 0);
                    for pull in round.split(',') {
                        let (count, color) = pull
                            .trim()
                            .split_once(' ')
                            .ok_or(format!("No ' ' in pull {pull}"))?;
                        let count = count.parse::<u32>()?;
                        match color {
                            "red" => r = count,
                            "green" => g = count,
                            "blue" => b = count,
                            x => Err(format!("Color {x} is invalid"))?,
                        };
                    }
                    Ok((r, g, b))
                })
                .try_fold((0, 0, 0), |a, x: Result<_>| {
                    x.map(|x| (max(a.0, x.0), max(a.1, x.1), max(a.2, x.2)))
                })?;
            Ok(Self {
                id,
                red,
                green,
                blue,
            })
        }
    }
    
    1 vote
  8. Comment on Valve doesn't sell ad space on Steam so it can make room for surprise hits: 'We don't think Steam should be pay-to-win' in ~games

    whispersilk
    Link Parent
    I think you misunderstood. My understanding of wervenyt's comment is that state-owned things are "publicly owned" and literally everything else is "privately owned." The difference is that...

    I think you misunderstood. My understanding of wervenyt's comment is that state-owned things are "publicly owned" and literally everything else is "privately owned." The difference is that "public" companies are "privately owned" collectively, by anyone who can afford to pay for stock, while "private" companies are "privately owned" by specific individual people and partial ownership can't be bought and sold.

    4 votes
  9. Comment on Possible solution for the perennial "what app" issue in ~tildes

    whispersilk
    Link Parent
    So I went and looked it up in the App Store and it appears that it's a web browser specifically optimized for and integrated with social media sites. So yes you can browse Facebook but you can...

    So I went and looked it up in the App Store and it appears that it's a web browser specifically optimized for and integrated with social media sites. So yes you can browse Facebook but you can also ask it to block ads or Stories on your timeline, or to sort posts chronologically, or to download videos you're watching — things that a general-purpose browser can't do because they involve giving Facebook special treatment. The same is true for other sites it has built-in support for. I imagine Tildes would get some specific benefit from it (for example, it looks like it allows you to set a color scheme that applies to all supported sites for a consistent experience (yes, I know Tildes lets you pick a color scheme; the benefit here is the cross-site consistency)) but not as much as Facebook does.

    @triadderall_triangle is what I've said accurate?

    8 votes
  10. Comment on Looking for book suggestions in ~books

    whispersilk
    Link Parent
    Seconding The House of the Scorpion! I read this book when I was a teenager and it still sticks with me. It's very, very good.

    Seconding The House of the Scorpion! I read this book when I was a teenager and it still sticks with me. It's very, very good.

    1 vote
  11. Comment on Reddit is removing ability to opt out of ad personalization based on your activity on the platform in ~tech

    whispersilk
    Link Parent
    They can't remove RES, but they could in theory become hostile to it, for example by making changes to the structure and semantics of the site HTML such that RES breaks and can't keep up with...

    They can't remove RES, but they could in theory become hostile to it, for example by making changes to the structure and semantics of the site HTML such that RES breaks and can't keep up with fixes. I also believe, though I'm not certain because I've had old.reddit.com set as my default since the redesign, that RES is primarily an old.reddit.com extension and has very limited functionality on new reddit.

    27 votes
  12. Comment on iOS 17 is available for iPhone users in ~tech

    whispersilk
    Link Parent
    This is the tactical-nuclear option, but it looks like you can go to Settings > General > Transfer or Reset iPhone > Reset > Reset Keyboard Dictionary. In the words of the Apple support article...

    This is the tactical-nuclear option, but it looks like you can go to Settings > General > Transfer or Reset iPhone > Reset > Reset Keyboard Dictionary.

    In the words of the Apple support article linked above:

    All custom words and shortcuts are deleted, and the keyboard dictionary returns to its default state.

    I think that will serve to "un-train" it like you're looking for.


    @hobbes64 for your information as well.

    2 votes
  13. Comment on iOS 17 is available for iPhone users in ~tech

    whispersilk
    Link Parent
    I've found that it's very willing to place the cursor without selecting except on words that it autocorrected. If you need to place the cursor on or in those words — and I know this isn't the...

    I've found that it's very willing to place the cursor without selecting except on words that it autocorrected. If you need to place the cursor on or in those words — and I know this isn't the quite the same — long-pressing on the space bar will let you freely move the cursor around and stick it wherever you want.

    2 votes
  14. Comment on Cult of the Lamb dev says it will delete the game on January 1 in ~games

    whispersilk
    Link Parent
    It's not retroactive in that it applies to downloads made in the past, but it is retroactive in that it applies to downloads of games that were made and released in the past. Imagine you used...

    It's not retroactive in that it applies to downloads made in the past, but it is retroactive in that it applies to downloads of games that were made and released in the past.

    Imagine you used Unity to make a game in 2019. You read the terms of service, you decided it was all good, you published the game. After 2019, you never touched the codebase again. Then, in 2024, your game gets very popular for some reason and you pass the threshold for this to take effect! Unity will now be charging you for every install, despite the fact that the per-install fee wasn't included in the terms of service when you agreed to them and despite the fact that you haven't so much as touched the engine in the past five years.

    30 votes
  15. Comment on Super Mario Bros. Any% Speedrun in 4:54.631 in ~games

    whispersilk
    Link Parent
    TAS stands for tool-assisted speedrun, which is a speedrun where a computer is utilized to actually perform the inputs when the game is running, which means that everything can happen with...

    TAS stands for tool-assisted speedrun, which is a speedrun where a computer is utilized to actually perform the inputs when the game is running, which means that everything can happen with frame-perfect accuracy. This speedrunner matched a computer for speed and accuracy up until world 8, level 4, and beat the game only four frames (~a tenth of a second) after the computer did. This speedrun was very, very close to literally perfect.

    7 votes
  16. Comment on What LitRPG Series do you recommend? in ~books

    whispersilk
    Link
    LitRPG is a tough genre for me. When I find one that clicks it's really good, but most just don't click, whether that's because they focus too much on the game system to the exclusion of all else,...

    LitRPG is a tough genre for me. When I find one that clicks it's really good, but most just don't click, whether that's because they focus too much on the game system to the exclusion of all else, because the game system itself just doesn't make sense for me, or for other reasons. Personal note aside, if you want to find more LitRPGs I would try RoyalRoad and ScribbleHub, both of which have tags for it. In my experience RoyalRoad is more Western and SciribbleHub has more wuxia/cultivation type stuff. Some specific recommendations:

    • Super Supportive. My current favorite! The worldbuilding and characters are so, so good.
    • Worth the Candle. Alexander Wales has done a lot to earn my trust that whatever they write will be good, and this was.
    • He Who Fights with Monsters is also on RoyalRoad if you want to read the newest book as it releases.
    6 votes
  17. Comment on Looking for a good note-taking app in ~tech

    whispersilk
    Link
    Is Syncthing off the table for your use case? I’m using it to sync an Obsidian vault across my desktop, laptop, and phone and it’s working great for me.

    Is Syncthing off the table for your use case? I’m using it to sync an Obsidian vault across my desktop, laptop, and phone and it’s working great for me.

    25 votes
  18. Comment on Recommendations and request for web serials in ~books

    whispersilk
    (edited )
    Link
    Worm was my introduction to web serials and is still my favorite. It just works for me in a way most other stories don’t. I read it over the course of a week in 2013 — for reference, it’s 1.7...

    Worm was my introduction to web serials and is still my favorite. It just works for me in a way most other stories don’t. I read it over the course of a week in 2013 — for reference, it’s 1.7 million words so it pulled me in hard — and it’s occupied a portion of my brain ever since.

    The only thing I put on the same level as it is Katalepsis. Katalepsis is an eldritch horror/urban fantasy story story that is extremely cool and has the distinction of an almost-entirely female, almost-entirely lesbian cast, which I love it for.

    I will heartily recommend both. They’re utterly fantastic.

    I’ll also throw out Pale, an urban fantasy story by the same author as Worm. I’m woefully behind, but people I trust who are up to date say it’s the author’s best work yet. As I understand it, Pale is longer than Worm and Katalepsis combined.


    Edit:

    Oh, also Unsong! It’s by the author of Slatestar Codex, if that means anything to you. God is real, His names have power, and capitalism has made an industry of trying to figure out what they are and patent them. Also the world is ending and the main character loves puns. It’s a fun ride. Much shorter than anything I’ve listed above.

    1 vote
  19. Comment on Books with WTF premises in ~books

    whispersilk
    Link Parent
    It brought All You Zombies to my mind as well! I’ve read that one and enjoyed it/have thought about it a lot since, but haven’t read The Man Who Folded Himself. The premise is definitely a...

    It brought All You Zombies to my mind as well! I’ve read that one and enjoyed it/have thought about it a lot since, but haven’t read The Man Who Folded Himself. The premise is definitely a fascinating one and I’m glad it hasn’t only been done once.

    1 vote
  20. Comment on Help me build my “woke” Fourth of July playlist in ~music

    whispersilk
    Link
    One that I haven’t seen elsewhere in the thread yet is “Land of the Free” by The Killers. Then on a more uplifting note there’s “Blueneck” by Chris Housman.

    One that I haven’t seen elsewhere in the thread yet is “Land of the Free” by The Killers.

    So how many daughters, tell me how many sons
    Do we have to put in the ground
    Before we just break down and face it
    We got a problem with guns
    In the land of the free

    Then on a more uplifting note there’s “Blueneck” by Chris Housman.

    Can a country kid wanna see the glass ceiling shatter?
    Wanna see a world where Black Lives Matter
    Liberty and Justice for just some of us
    Ain't how the heartland brought me up

    2 votes