use std::collections::HashMap;
use crate::{Arm, Cell, Pos, ShapeId, StrokeCell, Window};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StampMode {
Above,
Below,
}
pub trait StampSink {
fn stamp(&mut self, at: Pos, cell: Cell, mode: StampMode, owner: Option<ShapeId>);
}
pub trait CellReader {
#[must_use]
fn cell(&self, at: Pos) -> Option<&Cell>;
}
pub trait OwnerReader {
#[must_use]
fn owner(&self, at: Pos) -> Option<ShapeId>;
}
pub struct Buffer {
window: Window,
cells: HashMap<(i16, i16), Cell>,
owners: HashMap<(i16, i16), ShapeId>,
}
impl Buffer {
#[must_use]
pub fn new(window: Window) -> Self {
Self {
window,
cells: HashMap::new(),
owners: HashMap::new(),
}
}
#[must_use]
pub fn window(&self) -> Window {
self.window
}
fn contains(&self, at: Pos) -> bool {
self.window.contains(at)
}
}
impl CellReader for Buffer {
fn cell(&self, at: Pos) -> Option<&Cell> {
self.cells.get(&(at.x, at.y))
}
}
impl OwnerReader for Buffer {
fn owner(&self, at: Pos) -> Option<ShapeId> {
self.owners.get(&(at.x, at.y)).copied()
}
}
impl StampSink for Buffer {
fn stamp(&mut self, at: Pos, cell: Cell, mode: StampMode, owner: Option<ShapeId>) {
if !self.contains(at) {
return;
}
let decides_something = decides_a_side(&cell);
let takes = match self.cells.get_mut(&(at.x, at.y)) {
None => {
self.cells.insert((at.x, at.y), cell);
decides_something
}
Some(target) => {
*target = match mode {
StampMode::Above if cell.is_decided() => cell,
StampMode::Above => merge(cell, target),
StampMode::Below if target.is_decided() => return,
StampMode::Below => merge(target.clone(), &cell),
};
decides_something
&& match mode {
StampMode::Above => true,
StampMode::Below => !self.owners.contains_key(&(at.x, at.y)),
}
}
};
if takes && let Some(owner) = owner {
self.owners.insert((at.x, at.y), owner);
}
}
}
fn merge(top: Cell, bottom: &Cell) -> Cell {
match (top, bottom) {
(opaque @ (Cell::Literal(_) | Cell::Symbol(_)), _) => opaque,
(Cell::Strokes(stroke), Cell::Literal(_) | Cell::Symbol(_)) => {
close_unset_sides(stroke).into()
}
(Cell::Strokes(top), Cell::Strokes(bottom)) => merge_strokes(top, bottom).into(),
}
}
fn decides_a_side(cell: &Cell) -> bool {
match cell {
Cell::Literal(_) | Cell::Symbol(_) => true,
Cell::Strokes(cell) => {
!matches!(cell.top, Arm::Unset)
|| !matches!(cell.right, Arm::Unset)
|| !matches!(cell.bottom, Arm::Unset)
|| !matches!(cell.left, Arm::Unset)
}
}
}
fn close_unset_sides(cell: StrokeCell) -> StrokeCell {
let close = |arm| {
if matches!(arm, Arm::Unset) {
Arm::Closed
} else {
arm
}
};
StrokeCell {
base: cell.base,
top: close(cell.top),
right: close(cell.right),
bottom: close(cell.bottom),
left: close(cell.left),
}
}
fn merge_strokes(top: StrokeCell, bottom: &StrokeCell) -> StrokeCell {
StrokeCell {
base: top.base,
top: merge_arm(top.top, bottom.top.clone()),
right: merge_arm(top.right, bottom.right.clone()),
bottom: merge_arm(top.bottom, bottom.bottom.clone()),
left: merge_arm(top.left, bottom.left.clone()),
}
}
fn merge_arm(top: Arm, bottom: Arm) -> Arm {
if matches!(top, Arm::Unset) {
bottom
} else {
top
}
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU16;
use super::{Buffer, CellReader, OwnerReader, StampMode, StampSink};
use crate::{
Arm, BoxShape, Cell, FigureLayer, Glyph, Offset, Pos, Shape as _, ShapeId, Size, Stroke,
StrokeCell, Symbol, SymbolCell, SymbolStyleName, Window,
};
fn light() -> Stroke {
Stroke::from("light")
}
fn double() -> Stroke {
Stroke::from("double")
}
fn heavy() -> Stroke {
Stroke::from("heavy")
}
fn glyph(text: &str) -> Glyph {
Glyph::new(text).unwrap_or_else(|| panic!("{text:?} is one glyph"))
}
fn a_symbol_cell() -> Cell {
Cell::Symbol(SymbolCell {
symbol: Symbol::DirectionUp,
style: SymbolStyleName::from("solid"),
})
}
fn id(ordinal: u16) -> ShapeId {
ShapeId::new(NonZeroU16::new(ordinal).expect("no test asks for zero"))
}
fn a_buffer(origin: Pos, width: u16, height: u16) -> Buffer {
Buffer::new(Window {
origin,
size: Size { width, height },
})
}
fn decided() -> Cell {
Cell::from(StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
})
}
fn vertical_run() -> Cell {
Cell::from(StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Unset,
bottom: Arm::Set(light()),
left: Arm::Unset,
})
}
fn horizontal_run() -> Cell {
Cell::from(StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Unset,
left: Arm::Set(light()),
})
}
fn deciding_nothing() -> Cell {
Cell::from(StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Unset,
bottom: Arm::Unset,
left: Arm::Unset,
})
}
fn a_box(at: Pos, stroke: Stroke, fill: Option<&str>) -> BoxShape {
BoxShape {
at,
size: Size {
width: 4,
height: 3,
},
stroke,
fill: fill.map(glyph),
}
}
fn every_position(origin: Pos, width: u16, height: u16) -> Vec<Pos> {
(0..height)
.flat_map(|y| {
(0..width).map(move |x| Pos {
x: origin.x + i16::try_from(x).expect("a window this narrow"),
y: origin.y + i16::try_from(y).expect("a window this short"),
})
})
.collect()
}
#[test]
fn a_new_buffer_has_no_cell_anywhere() {
let buffer = Buffer::new(Window {
origin: Pos { x: -1, y: -1 },
size: Size {
width: 3,
height: 3,
},
});
for x in -2..3 {
for y in -2..3 {
assert!(buffer.cell(Pos { x, y }).is_none());
}
}
}
#[test]
fn stamping_outside_the_window_changes_nothing() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 2,
height: 1,
},
});
buffer.stamp(
Pos { x: 2, y: 0 },
StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
}
.into(),
StampMode::Above,
None,
);
assert!(buffer.cell(Pos { x: 2, y: 0 }).is_none());
}
#[test]
fn stamping_an_undefined_position_defines_it_entirely() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
let cell: Cell = StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Unset,
}
.into();
buffer.stamp(Pos { x: 0, y: 0 }, cell.clone(), StampMode::Above, None);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&cell));
}
#[test]
fn an_unset_arm_on_a_stamp_leaves_the_target_arm_alone() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Closed,
bottom: Arm::Closed,
left: Arm::Closed,
}
.into(),
StampMode::Above,
None,
);
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Set(light()),
}
.into(),
StampMode::Above,
None,
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
Some(&Cell::from(StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Set(light()),
}))
);
}
#[test]
fn stamping_outside_the_window_changes_nothing_under_below() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 2,
height: 1,
},
});
buffer.stamp(
Pos { x: 2, y: 0 },
StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
}
.into(),
StampMode::Below,
None,
);
assert!(buffer.cell(Pos { x: 2, y: 0 }).is_none());
}
#[test]
fn stamping_an_undefined_position_with_below_defines_it_entirely() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
let cell: Cell = StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Unset,
}
.into();
buffer.stamp(Pos { x: 0, y: 0 }, cell.clone(), StampMode::Below, None);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&cell));
}
#[test]
fn below_writes_only_the_targets_unset_sides_and_leaves_the_base_stroke_alone() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Unset,
}
.into(),
StampMode::Above,
None,
);
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: Stroke::from("double"),
top: Arm::Set(Stroke::from("double")),
right: Arm::Closed,
bottom: Arm::Set(Stroke::from("double")),
left: Arm::Unset,
}
.into(),
StampMode::Below,
None,
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
Some(&Cell::from(StrokeCell {
base: light(),
top: Arm::Set(Stroke::from("double")),
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Unset,
}))
);
}
#[test]
fn below_onto_a_decided_cell_changes_nothing() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
let decided: Cell = StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Closed,
bottom: Arm::Set(light()),
left: Arm::Closed,
}
.into();
buffer.stamp(Pos { x: 0, y: 0 }, decided.clone(), StampMode::Above, None);
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: Stroke::from("double"),
top: Arm::Set(Stroke::from("double")),
right: Arm::Set(Stroke::from("double")),
bottom: Arm::Set(Stroke::from("double")),
left: Arm::Set(Stroke::from("double")),
}
.into(),
StampMode::Below,
None,
);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&decided));
}
#[test]
fn above_with_a_decided_stamp_wins_outright() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: Stroke::from("double"),
top: Arm::Unset,
right: Arm::Set(Stroke::from("double")),
bottom: Arm::Unset,
left: Arm::Closed,
}
.into(),
StampMode::Above,
None,
);
let decided: Cell = StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Closed,
bottom: Arm::Set(light()),
left: Arm::Closed,
}
.into();
buffer.stamp(Pos { x: 0, y: 0 }, decided.clone(), StampMode::Above, None);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&decided));
}
#[test]
fn front_to_back_with_below_equals_back_to_front_with_above() {
let pos = Pos { x: 0, y: 0 };
let a = || {
Cell::from(StrokeCell {
base: Stroke::from("double"),
top: Arm::Unset,
right: Arm::Set(Stroke::from("double")),
bottom: Arm::Unset,
left: Arm::Closed,
})
};
let b = || {
Cell::from(StrokeCell {
base: Stroke::from("light"),
top: Arm::Set(Stroke::from("light")),
right: Arm::Closed,
bottom: Arm::Unset,
left: Arm::Set(Stroke::from("light")),
})
};
let c = || {
Cell::from(StrokeCell {
base: Stroke::from("heavy"),
top: Arm::Closed,
right: Arm::Set(Stroke::from("heavy")),
bottom: Arm::Set(Stroke::from("heavy")),
left: Arm::Set(Stroke::from("heavy")),
})
};
let mut front_to_back = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
front_to_back.stamp(pos, a(), StampMode::Below, None);
front_to_back.stamp(pos, b(), StampMode::Below, None);
front_to_back.stamp(pos, c(), StampMode::Below, None);
let mut back_to_front = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
back_to_front.stamp(pos, c(), StampMode::Above, None);
back_to_front.stamp(pos, b(), StampMode::Above, None);
back_to_front.stamp(pos, a(), StampMode::Above, None);
let expected = Some(&Cell::from(StrokeCell {
base: Stroke::from("double"),
top: Arm::Set(Stroke::from("light")),
right: Arm::Set(Stroke::from("double")),
bottom: Arm::Set(Stroke::from("heavy")),
left: Arm::Closed,
}));
assert_eq!(front_to_back.cell(pos), back_to_front.cell(pos));
assert_eq!(front_to_back.cell(pos), expected);
}
#[test]
fn a_literal_stamped_above_a_stroke_cell_replaces_it() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
}
.into(),
StampMode::Above,
None,
);
let literal = Cell::Literal(glyph("A"));
buffer.stamp(Pos { x: 0, y: 0 }, literal.clone(), StampMode::Above, None);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&literal));
}
#[test]
fn a_literal_stamped_below_a_stroke_cell_closes_its_unset_sides() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Set(light()),
}
.into(),
StampMode::Above,
None,
);
buffer.stamp(
Pos { x: 0, y: 0 },
Cell::Literal(glyph("A")),
StampMode::Below,
None,
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
Some(&Cell::from(StrokeCell {
base: light(),
top: Arm::Closed,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Set(light()),
}))
);
}
#[test]
fn a_stroke_cell_stamped_above_a_literal_inherits_its_closed_sides_where_it_abstains() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
Cell::Literal(glyph("A")),
StampMode::Above,
None,
);
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Set(light()),
}
.into(),
StampMode::Above,
None,
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
Some(&Cell::from(StrokeCell {
base: light(),
top: Arm::Closed,
right: Arm::Set(light()),
bottom: Arm::Closed,
left: Arm::Set(light()),
}))
);
}
#[test]
fn a_stroke_cell_stamped_below_a_literal_changes_nothing() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
let literal = Cell::Literal(glyph("A"));
buffer.stamp(Pos { x: 0, y: 0 }, literal.clone(), StampMode::Above, None);
buffer.stamp(
Pos { x: 0, y: 0 },
StrokeCell {
base: Stroke::from("double"),
top: Arm::Set(Stroke::from("double")),
right: Arm::Set(Stroke::from("double")),
bottom: Arm::Set(Stroke::from("double")),
left: Arm::Set(Stroke::from("double")),
}
.into(),
StampMode::Below,
None,
);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&literal));
}
#[test]
fn a_literal_stamped_above_a_literal_replaces_it() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
buffer.stamp(
Pos { x: 0, y: 0 },
Cell::Literal(glyph("A")),
StampMode::Above,
None,
);
let incoming = Cell::Literal(glyph("B"));
buffer.stamp(Pos { x: 0, y: 0 }, incoming.clone(), StampMode::Above, None);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&incoming));
}
#[test]
fn a_literal_stamped_below_a_literal_changes_nothing() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
let original = Cell::Literal(glyph("A"));
buffer.stamp(Pos { x: 0, y: 0 }, original.clone(), StampMode::Above, None);
buffer.stamp(
Pos { x: 0, y: 0 },
Cell::Literal(glyph("B")),
StampMode::Below,
None,
);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&original));
}
#[test]
fn a_symbol_cell_composes_as_a_literal_does() {
let pos = Pos { x: 0, y: 0 };
let mut named = a_buffer(pos, 1, 1);
named.stamp(pos, vertical_run(), StampMode::Above, None);
named.stamp(pos, a_symbol_cell(), StampMode::Above, None);
let mut spelled = a_buffer(pos, 1, 1);
spelled.stamp(pos, vertical_run(), StampMode::Above, None);
spelled.stamp(pos, Cell::Literal(glyph("▲")), StampMode::Above, None);
assert_eq!(
named.cell(pos),
Some(&a_symbol_cell()),
"the symbol replaces the stroke cell it was stamped over"
);
assert_eq!(spelled.cell(pos), Some(&Cell::Literal(glyph("▲"))));
named.stamp(pos, horizontal_run(), StampMode::Below, None);
spelled.stamp(pos, horizontal_run(), StampMode::Below, None);
assert_eq!(
named.cell(pos),
Some(&a_symbol_cell()),
"a stamp behind changes neither opaque kind"
);
assert_eq!(spelled.cell(pos), Some(&Cell::Literal(glyph("▲"))));
let incoming = Cell::Symbol(SymbolCell {
symbol: Symbol::Circle,
style: SymbolStyleName::from("solid"),
});
named.stamp(pos, incoming.clone(), StampMode::Above, None);
assert_eq!(
named.cell(pos),
Some(&incoming),
"two symbol cells compose as two literals do, the top one winning"
);
}
#[test]
fn front_to_back_with_below_equals_back_to_front_with_above_with_a_literal_in_the_middle() {
let pos = Pos { x: 0, y: 0 };
let s1 = || {
Cell::from(StrokeCell {
base: light(),
top: Arm::Unset,
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
})
};
let literal = || Cell::Literal(glyph("A"));
let s2 = || {
Cell::from(StrokeCell {
base: light(),
top: Arm::Set(light()),
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
})
};
let mut front_to_back = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
front_to_back.stamp(pos, s1(), StampMode::Below, None);
front_to_back.stamp(pos, literal(), StampMode::Below, None);
front_to_back.stamp(pos, s2(), StampMode::Below, None);
let mut back_to_front = Buffer::new(Window {
origin: Pos { x: 0, y: 0 },
size: Size {
width: 1,
height: 1,
},
});
back_to_front.stamp(pos, s2(), StampMode::Above, None);
back_to_front.stamp(pos, literal(), StampMode::Above, None);
back_to_front.stamp(pos, s1(), StampMode::Above, None);
let expected = Some(&Cell::from(StrokeCell {
base: light(),
top: Arm::Closed,
right: Arm::Set(light()),
bottom: Arm::Set(light()),
left: Arm::Set(light()),
}));
assert_eq!(front_to_back.cell(pos), back_to_front.cell(pos));
assert_eq!(front_to_back.cell(pos), expected);
}
#[test]
fn two_cells_that_render_the_same_are_the_same_cell_and_the_record_does_not_change_that() {
let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 2, 1);
let first = id(1);
let second = id(2);
let fill = Cell::Literal(glyph("░"));
buffer.stamp(
Pos { x: 0, y: 0 },
fill.clone(),
StampMode::Above,
Some(first),
);
buffer.stamp(
Pos { x: 1, y: 0 },
fill.clone(),
StampMode::Below,
Some(second),
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
buffer.cell(Pos { x: 1, y: 0 }),
"the same cell value written by two identities is one cell value"
);
assert_eq!(buffer.cell(Pos { x: 0, y: 0 }), Some(&fill));
assert_eq!(buffer.owner(Pos { x: 0, y: 0 }), Some(first));
assert_eq!(
buffer.owner(Pos { x: 1, y: 0 }),
Some(second),
"the record tells the two apart where the cell cannot"
);
}
#[test]
fn in_an_overlap_every_position_resolves_to_the_front_most_shape_that_decided_it() {
let origin = Pos { x: -3, y: -2 };
let mut buffer = a_buffer(origin, 7, 5);
let back = id(1);
let front = id(2);
let outside = id(3);
a_box(Pos { x: -2, y: -1 }, double(), Some("░")).draw(&mut FigureLayer::stamped_by(
&mut buffer,
StampMode::Below,
front,
));
a_box(Pos { x: 0, y: 0 }, light(), None).draw(&mut FigureLayer::stamped_by(
&mut buffer,
StampMode::Below,
back,
));
a_box(Pos { x: 6, y: 3 }, heavy(), None).draw(&mut FigureLayer::stamped_by(
&mut buffer,
StampMode::Below,
outside,
));
let decided_by_front = |at: Pos| (-2..=1).contains(&at.x) && (-1..=1).contains(&at.y);
let decided_by_back = |at: Pos| {
(0..=3).contains(&at.x)
&& (0..=2).contains(&at.y)
&& (at.x == 0 || at.x == 3 || at.y == 0 || at.y == 2)
};
for at in every_position(origin, 7, 5) {
let expected = if decided_by_front(at) {
Some(front)
} else if decided_by_back(at) {
Some(back)
} else {
None
};
assert_eq!(
buffer.owner(at),
expected,
"the record names {expected:?} at {at:?}"
);
}
}
#[test]
fn a_shape_that_stamps_a_position_without_changing_anything_there_does_not_take_it() {
let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 1, 1);
let first = id(1);
let second = id(2);
buffer.stamp(Pos { x: 0, y: 0 }, decided(), StampMode::Above, Some(first));
let before = buffer.cell(Pos { x: 0, y: 0 }).cloned();
assert!(
before.is_some(),
"the first stamp wrote the cell this test reads back"
);
buffer.stamp(
Pos { x: 0, y: 0 },
Cell::from(StrokeCell {
base: heavy(),
top: Arm::Set(heavy()),
right: Arm::Set(heavy()),
bottom: Arm::Set(heavy()),
left: Arm::Set(heavy()),
}),
StampMode::Below,
Some(second),
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }).cloned(),
before,
"a stamp below a decided cell reproduces it exactly, so the cell cannot have moved"
);
assert_eq!(
buffer.owner(Pos { x: 0, y: 0 }),
Some(first),
"the stamp behind changed nothing there, so it owns nothing there"
);
}
#[test]
fn a_stamp_that_decides_no_side_takes_nothing_and_leaves_the_owner_it_found() {
let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 1, 1);
let nobody = id(1);
let second = id(2);
buffer.stamp(
Pos { x: 0, y: 0 },
deciding_nothing(),
StampMode::Above,
Some(nobody),
);
assert_eq!(
buffer.owner(Pos { x: 0, y: 0 }),
None,
"a cell deciding no side is written like any other and owns nothing"
);
buffer.stamp(
Pos { x: 0, y: 0 },
horizontal_run(),
StampMode::Above,
Some(second),
);
let mut held = a_buffer(Pos { x: 0, y: 0 }, 1, 1);
let holder = id(3);
held.stamp(
Pos { x: 0, y: 0 },
horizontal_run(),
StampMode::Above,
Some(holder),
);
held.stamp(
Pos { x: 0, y: 0 },
deciding_nothing(),
StampMode::Above,
Some(second),
);
assert_eq!(
held.cell(Pos { x: 0, y: 0 }),
Some(&horizontal_run()),
"a stamp deciding no side changes nothing whatever it lands on"
);
assert_eq!(
held.owner(Pos { x: 0, y: 0 }),
Some(holder),
"the owner it found is the owner it leaves"
);
}
#[test]
fn a_shape_behind_cannot_take_a_cell_from_one_in_front_of_it_however_much_it_decides() {
let mut buffer = a_buffer(Pos { x: 0, y: 0 }, 1, 1);
let front = id(1);
let behind = id(2);
buffer.stamp(
Pos { x: 0, y: 0 },
vertical_run(),
StampMode::Above,
Some(front),
);
buffer.stamp(
Pos { x: 0, y: 0 },
horizontal_run(),
StampMode::Below,
Some(behind),
);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
Some(&decided()),
"the shape behind decided the two sides the one in front left open"
);
assert_eq!(
buffer.owner(Pos { x: 0, y: 0 }),
Some(front),
"and the record names the one in front, because it reached the cell first"
);
}
#[test]
fn the_record_is_the_same_whether_a_diagram_is_drawn_front_to_back_or_back_to_front() {
let origin = Pos { x: 0, y: 0 };
let back = id(1);
let front = id(2);
let mut front_to_back = a_buffer(origin, 6, 4);
a_box(Pos { x: 2, y: 1 }, double(), Some("▓")).draw(&mut FigureLayer::stamped_by(
&mut front_to_back,
StampMode::Below,
front,
));
a_box(Pos { x: 0, y: 0 }, light(), Some("░")).draw(&mut FigureLayer::stamped_by(
&mut front_to_back,
StampMode::Below,
back,
));
let mut back_to_front = a_buffer(origin, 6, 4);
a_box(Pos { x: 0, y: 0 }, light(), Some("░")).draw(&mut FigureLayer::stamped_by(
&mut back_to_front,
StampMode::Above,
back,
));
a_box(Pos { x: 2, y: 1 }, double(), Some("▓")).draw(&mut FigureLayer::stamped_by(
&mut back_to_front,
StampMode::Above,
front,
));
for at in every_position(origin, 6, 4) {
assert_eq!(
front_to_back.cell(at),
back_to_front.cell(at),
"the two orders disagree about the cell at {at:?}"
);
assert_eq!(
front_to_back.owner(at),
back_to_front.owner(at),
"the two orders disagree about who owns the position {at:?}"
);
}
for at in [Pos { x: 2, y: 1 }, Pos { x: 3, y: 2 }] {
assert_eq!(
front_to_back.owner(at),
Some(front),
"the shape in front owns the cell it decided"
);
}
}
#[test]
fn owner_answers_for_a_position_and_none_for_one_nobody_holds() {
let origin = Pos { x: -1, y: -1 };
let mut buffer = a_buffer(origin, 2, 2);
let writer = id(1);
buffer.stamp(
Pos { x: 0, y: 0 },
decided(),
StampMode::Above,
Some(writer),
);
for at in every_position(origin, 2, 2) {
let written = at == (Pos { x: 0, y: 0 });
assert_eq!(
buffer.owner(at),
written.then_some(writer),
"{at:?} is the one position written, and only it is owned"
);
}
for at in [
Pos { x: 1, y: 0 },
Pos { x: -1, y: 1 },
Pos { x: 1, y: 1 },
Pos {
x: i16::MAX,
y: i16::MAX,
},
] {
assert_eq!(
buffer.owner(at),
None,
"{at:?} is outside a two-by-two window"
);
}
}
#[test]
fn a_caller_that_adds_the_windows_origin_to_a_click_asks_about_another_cell() {
let origin = Pos { x: -2, y: -1 };
let mut buffer = a_buffer(origin, 4, 4);
let writer = id(1);
let clicked = Offset { x: 2, y: 3 };
buffer.stamp(
Pos { x: 0, y: 2 },
decided(),
StampMode::Above,
Some(writer),
);
let at = buffer
.window()
.position(clicked)
.expect("the click reaches a position");
assert_eq!(
buffer.owner(at),
Some(writer),
"the click reaches the cell as written"
);
assert_eq!(
buffer.owner(Pos {
x: at.x + origin.x,
y: at.y + origin.y,
}),
None,
"adding the origin a second time asks about a cell nobody clicked"
);
}
#[test]
fn a_window_at_i16_min_carries_every_offset_and_owns_nothing_outside_it() {
let mut buffer = a_buffer(Pos { x: i16::MIN, y: 0 }, 4, 3);
a_box(Pos { x: 0, y: 0 }, light(), Some("░")).draw(&mut FigureLayer::stamped_by(
&mut buffer,
StampMode::Below,
id(1),
));
let window = buffer.window();
for y in 0..3 {
for x in 0..5 {
let at = window
.position(Offset { x, y })
.expect("a window at i16::MIN carries every offset");
assert_eq!(
buffer.owner(at),
None,
"{at:?} is not a cell the box wrote, which was drawn far from this window"
);
}
}
let past = window
.position(Offset { x: u16::MAX, y: 0 })
.expect("a window at i16::MIN carries every offset");
assert_eq!(buffer.owner(past), None);
assert_eq!(
buffer.cell(Pos { x: 0, y: 0 }),
None,
"and no cell was written there either, so nothing is owned in the first place"
);
}
#[test]
fn a_shape_drawn_by_nobody_owns_nothing_even_where_it_writes() {
let mut by_nobody = a_buffer(Pos { x: 0, y: 0 }, 4, 3);
a_box(Pos { x: 0, y: 0 }, light(), Some("░"))
.draw(&mut FigureLayer::new(&mut by_nobody, StampMode::Above));
let mut by_somebody = a_buffer(Pos { x: 0, y: 0 }, 4, 3);
let writer = id(1);
a_box(Pos { x: 0, y: 0 }, light(), Some("░")).draw(&mut FigureLayer::stamped_by(
&mut by_somebody,
StampMode::Above,
writer,
));
let mut wrote_something = false;
for at in every_position(Pos { x: 0, y: 0 }, 4, 3) {
assert!(by_nobody.cell(at).is_some(), "the box wrote {at:?}");
wrote_something = true;
assert_eq!(
by_nobody.owner(at),
None,
"a shape drawn by nobody owns nothing at {at:?}"
);
assert_eq!(
by_somebody.owner(at),
Some(writer),
"the same box under an identity owns every cell it decided"
);
}
assert!(
wrote_something,
"the box above wrote nothing, so this test proved nothing"
);
}
}