Browse Source

It's a little more of a game!

master
Graham Northup 6 months ago
parent
commit
44f6e2c56e
  1. 7
      Cargo.toml
  2. 44
      src/ent.rs
  3. 2
      src/gen.rs
  4. 43
      src/main.rs
  5. 138
      src/world/entity/mod.rs
  6. 55
      src/world/entity/player.rs
  7. 243
      src/world/mod.rs
  8. 268
      src/world/render.rs

7
Cargo.toml

@ -1,14 +1,15 @@
[package]
name = "rogue-engine"
name = "spring2022"
version = "0.1.0"
authors = ["thajohns <thajohns@clarkson.edu>", "The Notorious Thug of the North <northug@clarkson.edu>"]
edition = "2018"
[dependencies]
rogue_util = { git = "https://github.com/grissess/rogue_utils" }
rogue-engine = { git = "https://gitea.cslabs.clarkson.edu/thajohns/7hrl-engine.git" }
termion = "^1"
rand_distr = "^0.2"
rand_distr = "^0.4"
[dependencies.rand]
version = "^0.7"
version = "^0.8"
features = ["small_rng"]

44
src/ent.rs

@ -0,0 +1,44 @@
use rogue_engine::world::entity::{Entity, Hp, Observation, Action, ECLS_PROTAG, DamageDesc, DamageKind, ai::violence_toward};
use rogue_util::coord::V2i;
use std::rc::Rc;
macro_rules! make_entity {
($name:ident, { $( $field:tt ),* }, { $( $methods:tt )* }) => {
#[derive(Debug)]
pub struct $name {
pos: V2i,
hp: Hp,
time: usize,
$( $field ),*
}
impl Entity for $name {
fn pos(&self) -> V2i { self.pos }
fn set_pos(&mut self, pos: V2i) { self.pos = pos; }
fn next_time(&self) -> usize { self.time }
fn set_next_time(&mut self, time: usize) { self.time = time; }
fn hp(&self) -> Hp { self.hp }
fn set_hp(&mut self, hp: Hp) { self.hp = hp; }
$( $methods )*
}
}
}
make_entity!(Prowler, {}, {
fn act<'w>(&mut self, obs: &Observation<'w>) -> Action {
violence_toward(self.pos, obs, |ent| ent.borrow().class() & ECLS_PROTAG != 0, |ent| {
Rc::new(vec![DamageDesc{ kind: DamageKind::Phys, base: 6, add: 6 }])
})
}
});
impl Prowler {
pub fn new(pos: V2i) -> Self {
Prowler {
pos,
hp: 50,
time: 0,
}
}
}

2
src/world/gen.rs → src/gen.rs

@ -3,7 +3,7 @@ use std::ops::Div;
use rogue_util::coord::{R2i, V2i};
use rogue_util::grid::{Grid};
use rogue_util::raster;
use super::{Tile, Content};
use rogue_engine::world::{Tile, Content};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng, distributions::WeightedIndex};
use rand_distr::{Poisson, Distribution, Uniform, Bernoulli};

43
src/main.rs

@ -1,22 +1,41 @@
extern crate termion;
extern crate rogue_util;
extern crate rogue_engine;
extern crate rand;
pub mod world;
pub mod gen;
pub mod ent;
use rogue_util::{coord::*, grid::path::Traversable};
use termion::{raw::IntoRawMode, input::TermRead};
use std::io::{self, Write};
use std::rc::Rc;
use std::cell::RefCell;
use world::World;
use rogue_engine::world::{self, World, entity::{EntRef, ent_ref}};
fn main() -> io::Result<()>
{
let stdin = io::stdin();
let mut stdout = io::stdout().into_raw_mode().unwrap();
let mut w = World::new(0, stdin.keys());
let w = Rc::new(RefCell::new(World::new(0, stdin.keys())));
let newents: Rc<RefCell<Vec<EntRef>>> = Rc::new(RefCell::new(Vec::new()));
let borrowed_newents = Rc::clone(&newents);
w.borrow_mut().gen_cb.borrow_mut().push(Box::new(move |_, _, g| {
for pt in g.rect().iter() {
if g.get(pt).unwrap().can_pass() {
if rand::random::<f32>() < 0.005 {
borrowed_newents.borrow_mut().push(ent_ref(ent::Prowler::new(pt)));
break;
}
}
}
}));
let ppos: V2i = {
let mut rg = w.region_mut();
let wg = w.borrow_mut();
let mut rg = wg.region_mut();
let mut ps: Option<V2i> = None;
for pt in &R2i::origin_dim(V2i(0, 0), rg.grid_size()) {
@ -30,15 +49,24 @@ fn main() -> io::Result<()>
ps.unwrap()
};
w.player_mut().set_pos(ppos);
w.borrow_mut().player_mut().set_pos(ppos);
let (width, height) = termion::terminal_size()?;
let mut rs = world::render::RenderState::new(V2i(width as isize, height as isize));
let mut died = false;
loop {
w.render(&mut rs, &mut stdout)?;
w.borrow().render(&mut rs, &mut stdout)?;
print!("{}HP: {}",
termion::cursor::Goto(1, height),
w.borrow().player().hp(),
);
stdout.flush()?;
if let Some(()) = w.step() { break; }
if let Some(()) = w.borrow_mut().step() { break; }
if w.borrow().player().is_dead() { died = true; break; }
for eref in newents.borrow_mut().drain(..) {
w.borrow_mut().add_entity(eref);
}
}
print!("{}{}",
@ -47,6 +75,7 @@ fn main() -> io::Result<()>
);
std::mem::drop(stdout);
if died { println!("you died!"); }
println!("insert finalization here");
io::stdout().flush()?;
Ok(())

138
src/world/entity/mod.rs

@ -1,138 +0,0 @@
pub mod player;
use super::*;
use rogue_util::{coord::*, raster, grid::Grid};
use std::fmt::Debug;
pub trait Visible {
fn is_transparent(&self) -> bool;
}
impl Visible for Tile {
fn is_transparent(&self) -> bool { self.content == Content::Air }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LinfDir {
UL, U, UR,
L, R,
DL, D, DR,
}
impl LinfDir {
pub fn to_v2i(self) -> V2i {
use LinfDir::*;
match self {
UL => V2i(-1, -1),
U => V2i(0, -1),
UR => V2i(1, -1),
L => V2i(-1, 0),
R => V2i(1, 0),
DL => V2i(-1, 1),
D => V2i(0, 1),
DR => V2i(1, 1),
}
}
}
#[derive(Debug, Clone)]
pub enum Action {
Move(LinfDir),
Wait,
Quit,
}
impl Default for Action {
fn default() -> Action { Action::Wait }
}
#[derive(Debug, Clone)]
pub struct Observation {
pub sight: SightObs
}
impl Observation {
pub fn new(world: &World, v: V2i, sr: usize) -> Observation {
Observation {
sight: SightObs::new(world, v, sr),
}
}
}
#[derive(Debug, Clone)]
pub struct SightObs {
pub tiles: Grid<Option<V2i>>, // NB: Relative coordinate; 0,0 is this
pub entities: Vec<usize>, // NB: Entity indices
}
impl SightObs {
pub fn new(world: &World, v: V2i, sr: usize) -> SightObs {
let srsq = (sr as Vi) * (sr as Vi);
SightObs {
tiles: Grid::from_generator(|p| {
if p.l2_sq() > srsq {
return None;
}
{
let mut rg = world.region_mut();
for pt in raster::line(v, v + p) {
let tile = rg.get_or_create(pt);
if !tile.is_transparent() {
return None;
}
}
}
Some(v + p)
}, V2i(-(sr as Vi), -(sr as Vi)), V2i(2*sr as Vi, 2*sr as Vi)).unwrap(),
entities: world.entities().enumerate().filter_map(|(idx, eref)| {
let e = eref.borrow();
let pos = e.pos();
if (pos - v).l2_sq() > srsq {
return None;
}
{
let mut rg = world.region_mut();
for pt in raster::line(v, pos) {
let tile = rg.get_or_create(pt);
if !tile.is_transparent() {
return None;
}
}
}
Some(idx)
}).collect(),
}
}
}
const DEFAULT_SIGHT_RADIUS: usize = 10;
const DEFAULT_ACT_TIME: usize = 10000;
const ECLS_LIVING: usize = 1;
pub trait Entity {
fn pos(&self) -> V2i;
fn set_pos(&mut self, v: V2i);
fn next_time(&self) -> usize;
fn set_next_time(&mut self, t: usize);
fn class(&self) -> usize { 0 }
fn hp(&self) -> isize { 100 }
fn set_hp(&mut self, _hp: isize) {}
fn char(&self) -> char { 'E' }
fn sight(&self) -> usize { DEFAULT_SIGHT_RADIUS }
fn act_time(&self) -> usize { DEFAULT_ACT_TIME }
fn is_dead(&self) -> bool { self.hp() <= 0 }
fn act(&mut self, _obs: &Observation) -> Action { Action::Wait }
}
pub type EntBox = Box<dyn Entity>;
pub type EntCell = RefCell<EntBox>;
pub type EntRef = Rc<EntCell>;
pub type EntWeak = Weak<EntCell>;

55
src/world/entity/player.rs

@ -1,55 +0,0 @@
use super::*;
use rogue_util::coord::*;
use termion::event::Key;
use std::io;
pub struct Player<R> {
pos: V2i,
hp: usize, // TODO
time: usize,
input: R,
}
impl<R> Player<R> {
pub fn new(p: V2i, r: R) -> Player<R> {
Player {
pos: p,
hp: 100,
time: 0,
input: r,
}
}
}
impl<R> Entity for Player<R>
where
R: Iterator<Item=io::Result<Key>>
{
fn pos(&self) -> V2i { self.pos }
fn set_pos(&mut self, p: V2i) { self.pos = p; }
fn next_time(&self) -> usize { self.time }
fn set_next_time(&mut self, t: usize) { self.time = t; }
fn class(&self) -> usize {
ECLS_LIVING
}
fn char(&self) -> char { '@' }
fn act(&mut self, _obs: &Observation) -> Action {
use termion::event::Key::*;
loop {
match self.input.next().unwrap().unwrap() {
Char('h') | Char('a') | Left => return Action::Move(LinfDir::L),
Char('j') | Char('s') | Down => return Action::Move(LinfDir::D),
Char('k') | Char('w') | Up => return Action::Move(LinfDir::U),
Char('l') | Char('d') | Right => return Action::Move(LinfDir::R),
Char('y') => return Action::Move(LinfDir::UL),
Char('u') => return Action::Move(LinfDir::UR),
Char('b') => return Action::Move(LinfDir::DL),
Char('n') => return Action::Move(LinfDir::DR),
Char('q') => return Action::Quit,
_ => ()
}
}
}
}

243
src/world/mod.rs

@ -1,243 +0,0 @@
pub mod render;
pub mod gen;
pub mod entity;
use rogue_util::{coord::*, grid::{Grid, region::{Region, RegionConfig}, path::Traversable}};
use std::cell::RefCell;
use std::io::{self, Write};
use std::ops::{Deref, DerefMut};
use std::fmt::Debug;
use std::rc::{Rc, Weak};
use entity::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Content {
Air,
Stone,
}
impl Default for Content {
fn default() -> Content {
Content::Stone
}
}
#[derive(Debug, Clone)]
pub struct Tile {
pub content: Content,
}
impl Default for Tile {
fn default() -> Tile {
Tile {
content: Default::default(),
}
}
}
impl Traversable for Tile {
fn can_pass(&self) -> bool {
self.content == Content::Air
}
}
pub struct World {
cur_region: usize,
regions: Vec<RefCell<Region<Tile>>>,
entities: Vec<EntRef>, // NB: Player is ent 0
player: EntRef, // Kept alive by virtue of this field
}
impl World {
pub fn new<R>(seed: u64, r: R) -> World
where
R: Iterator<Item=io::Result<termion::event::Key>> + 'static
{
let start_region = RegionConfig::default()
.with_grid_gen(Some(Box::new(move |_, r, o, d| {
let mut g = Grid::from_default(o, d).expect("Failed to generate Grid");
gen::carve(seed, &mut g, r);
g
})))
.build().expect("Failed to make World");
// TODO: Worldgen on start_region
let player: EntRef = Rc::new(RefCell::new(Box::new(
entity::player::Player::new(V2i(0, 0), r)
)));
World {
cur_region: 0,
regions: vec![RefCell::new(start_region)],
entities: vec![
player.clone(),
],
player,
}
}
pub fn region<'s>(&'s self) -> impl Deref<Target=Region<Tile>> + 's {
self.regions[self.cur_region].borrow()
}
pub fn region_mut<'s>(&'s self) -> impl DerefMut<Target=Region<Tile>> + 's {
self.regions[self.cur_region].borrow_mut()
}
pub fn player<'s>(&'s self) -> impl Deref<Target=EntBox> + 's {
self.entities[0].borrow()
}
pub fn player_mut<'s>(&'s mut self) -> impl DerefMut<Target=EntBox> + 's {
self.entities[0].borrow_mut()
}
pub fn entities(&self) -> impl Iterator<Item=&EntRef> {
self.entities.iter()
}
pub fn entity<'s>(&'s self, idx: usize) -> impl Deref<Target=EntBox> + 's{
self.entities[idx].borrow()
}
pub fn entity_mut<'s>(&'s self, idx: usize) -> impl DerefMut<Target=EntBox> + 's {
self.entities[idx].borrow_mut()
}
pub fn add_entity(&mut self, ent: EntBox) -> EntWeak {
let ent = Rc::new(RefCell::new(ent));
self.entities.push(Rc::clone(&ent));
Rc::downgrade(&ent)
}
pub fn render<W: Write>(&self, rs: &mut render::RenderState, mut out: &mut W) -> io::Result<()> {
let ppos = self.player().pos();
let center = rs.center();
if (ppos - center).l2_sq() > 225 {
rs.recenter(ppos);
}
rs.render(&self.region(), &mut render::EdgeRenderer, &mut out)?;
let sr = rs.screen_rect();
for eref in self.entities() {
let e = eref.borrow();
let pos = e.pos();
if sr.contains(pos) {
rs.go_to_pt(pos, out)?;
write!(out, "{}", e.char())?;
}
}
Ok(())
}
pub fn step(&mut self) -> Option<()> {
let mut ents: Vec<_> = self.entities().enumerate().map(|(i, eref)| {
(i, eref.borrow().next_time())
}).collect();
ents.sort_by_key(|&(_, t)| t);
if let Some((i, t)) = ents.pop() {
let (pos, sr) = {
let mut e = self.entity_mut(i);
let pos = e.pos();
let sr = e.sight();
let dt = e.act_time();
e.set_next_time(t + dt);
(pos, sr)
};
let obs = entity::Observation::new(self as &World, pos, sr);
{
let mut e = self.entity_mut(i);
let act = e.act(&obs);
self.perform_act(e, act)
}
} else { Some(()) }
}
pub fn perform_act<E: DerefMut<Target=Box<dyn Entity>>>(&self, mut e: E, act: entity::Action) -> Option<()> {
use entity::{Action::*};
match act {
Wait => (),
Move(d) => {
let p = e.pos();
let dp = d.to_v2i();
{
let mut rg = self.region_mut();
let tile = rg.get_or_create(p + dp);
if !tile.can_pass() {
return None;
}
}
e.set_pos(p + dp);
},
Quit => {
return Some(());
},
}
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn room_align() -> io::Result<()> {
let mut w = World::new(0, Vec::new().into_iter());
{
let mut r = w.region_mut();
let gs = r.grid_size();
for v in R2i::origin_dim(V2i(-10, -2), V2i(20, 5)).iter()
{
r.get_or_create(v * gs);
}
//r.get_or_create(V2i(0, 0));
//r.get_or_create(V2i(0, gs.1));
//r.get_or_create(V2i(gs.0, 0));
//r.get_or_create(gs);
//r.get_or_create(V2i(0, 0) - gs);
//r.get_or_create(V2i(0, -gs.1));
//r.get_or_create(V2i(-gs.0, 0));
}
let (width, height) = termion::terminal_size()?;
for ii in 0..height { println!(""); }
let rs = render::RenderState::new(V2i(width as isize, height as isize));
let mut out = std::io::stdout();
let r = w.region();
rs.render(&r, &mut render::InvertRenderer, &mut out)?;
rs.draw_unpop(&r, &mut out)?;
rs.draw_grid(&r, &mut out)
}
#[test]
fn world_with_entity() -> io::Result<()> {
let mut w = World::new(0, Vec::new().into_iter());
{
let mut r = w.region_mut();
r.get_or_create(V2i(0, 0));
}
let (width, height) = termion::terminal_size()?;
let mut rs = render::RenderState::new(V2i(width as isize, height as isize));
let mut out = std::io::stdout();
#[derive(Debug)]
struct Ent;
impl Entity for Ent {
fn pos(&self) -> V2i { V2i(0, 0) }
fn set_pos(&mut self, v: V2i) {}
fn next_time(&self) -> usize { 0 }
fn set_next_time(&mut self, t: usize) {}
}
w.add_entity(Box::new(Ent));
w.render(&mut rs, &mut out)
}
}

268
src/world/render.rs

@ -1,268 +0,0 @@
use super::*;
use std::fmt::Debug;
use std::io::{self, Write};
use rogue_util::coord::*;
use rogue_util::grid::region::Region;
#[derive(Debug)]
pub struct Neighbors<'a, T: Debug> {
ul: Option<&'a T>, u: Option<&'a T>, ur: Option<&'a T>,
l: Option<&'a T>, this: Option<&'a T>, r: Option<&'a T>,
dl: Option<&'a T>, d: Option<&'a T>, dr: Option<&'a T>,
}
impl<'a, T: Debug + Default> Neighbors<'a, T> {
pub fn from_region_pos<'r: 'a>(r: &'r Region<T>, p: V2i, _def: &'a T) -> Neighbors<'a, T> {
Neighbors {
ul: r.get(p + V2i(-1, -1)),
u: r.get(p + V2i(0, -1)),
ur: r.get(p + V2i(1, -1)),
l: r.get(p + V2i(-1, 0)),
this: r.get(p),
r: r.get(p + V2i(1, 0)),
dl: r.get(p + V2i(-1, 1)),
d: r.get(p + V2i(0, 1)),
dr: r.get(p + V2i(1, 1)),
}
}
}
impl<'a, T: Debug> Neighbors<'a, T> {
pub fn for_each<F>(&self, mut f: F)
where
F: FnMut(Option<&'a T>)
{
f(self.ul); f(self.u); f(self.ur);
f(self.l); f(self.this); f(self.r);
f(self.dl); f(self.d); f(self.dr);
}
}
pub trait Renderer {
type Tile: Debug;
fn render<'a, 's: 'a>(&'s mut self, neighbors: Neighbors<'a, Self::Tile>) -> u8;
}
pub struct BasicRenderer;
impl Renderer for BasicRenderer {
type Tile = Tile;
fn render<'a, 's: 'a>(&'s mut self, neighbors: Neighbors<'a, Tile>) -> u8 {
match neighbors.this.map(|t| t.content) {
Some(Content::Air) => b'.',
Some(Content::Stone) | None => b' ',
}
}
}
pub struct InvertRenderer;
impl Renderer for InvertRenderer {
type Tile = Tile;
fn render<'a, 's: 'a>(&'s mut self, neighbors: Neighbors<'a, Tile>) -> u8 {
match neighbors.this.map(|t| t.content) {
Some(Content::Air) => b' ',
Some(Content::Stone) | None => b'#',
}
}
}
pub struct EdgeRenderer;
impl Renderer for EdgeRenderer {
type Tile = Tile;
fn render<'a, 's: 'a>(&'s mut self, neighbors: Neighbors<'a, Tile>) -> u8 {
match neighbors.this.map(|t| t.content) {
Some(Content::Air) => b'.',
None => b' ',
Some(Content::Stone) => {
let mut air_count = 0;
neighbors.for_each(
|t| if let Some(Tile { content: Content::Air, .. }) = t { air_count += 1; });
if air_count > 0 {
b'#'
} else {
b' '
}
},
}
}
}
pub struct RenderState {
term_size: RefCell<V2i>,
center: V2i,
}
impl RenderState {
pub fn new_centered(term_size: V2i, center: V2i) -> RenderState {
debug_assert!(term_size.is_strict_q1());
RenderState {
term_size: RefCell::new(term_size),
center,
}
}
pub fn new(term_size: V2i) -> RenderState { RenderState::new_centered(term_size, V2i(0, 0)) }
pub fn center(&self) -> V2i { self.center }
pub fn term_size(&self) -> V2i { self.term_size.borrow().clone() }
pub fn refresh_term_size(&self) -> io::Result<()> { let (w, h) = termion::terminal_size()?; *self.term_size.borrow_mut() = V2i(w as isize, h as isize); Ok(()) }
pub fn recenter(&mut self, c: V2i) {
self.center = c;
}
pub fn screen_rect(&self) -> R2i {
let ts = self.term_size();
let halfsize = ts / V2i(2, 2);
R2i::origin_dim(self.center - halfsize, ts)
}
pub fn render<R, W>(&self, region: &Region<Tile>, renderer: &mut R, out: &mut W) -> io::Result<()>
where
R: Renderer<Tile=Tile>,
W: Write
{
self.refresh_term_size()?;
let screen = self.screen_rect();
write!(out, "{}{}",
termion::clear::All,
termion::cursor::Goto(1, 1),
)?;
let def_tile = Tile::default();
let mut screen_buffer: Vec<u8> = Vec::new();
for pt in &screen {
screen_buffer.push(renderer.render(Neighbors::from_region_pos(region, pt, &def_tile)));
}
write!(out, "{}", unsafe { core::str::from_utf8_unchecked(&screen_buffer) })?;
Ok(())
}
pub fn go_to_pt<W: Write>(&self, pt: V2i, out: &mut W) -> io::Result<()> {
let spt = pt - self.screen_rect().origin() + V2i(1, 1);
write!(out, "{}", termion::cursor::Goto(spt.0 as u16, spt.1 as u16))
}
pub fn draw_grid<W: Write>(&self, region: &Region<Tile>, out: &mut W) -> io::Result<()> {
let screen = self.screen_rect();
let gs = region.grid_size();
for pt in &screen {
if pt.0.rem_euclid(gs.0) == 0 || pt.1.rem_euclid(gs.1) == 0 {
self.go_to_pt(pt, out)?;
write!(out, "+")?;
}
}
Ok(())
}
pub fn draw_unpop<W: Write>(&self, region: &Region<Tile>, out: &mut W) -> io::Result<()> {
let screen = self.screen_rect();
for pt in &screen {
if !region.is_populated_tile(pt) {
self.go_to_pt(pt, out)?;
write!(out, "?")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use rogue_util::grid::region::RegionConfig;
fn make_test_renderer() -> io::Result<RenderState> {
let (w, h) = termion::terminal_size()?;
Ok(RenderState::new(V2i(w as isize, h as isize)))
}
fn make_test_region() -> Region<Tile> {
RegionConfig::<Tile>::default().build().expect("Failed to make Region")
}
fn draw_line(rg: &mut Region<Tile>) {
let gs = rg.grid_size();
for pt in rogue_util::raster::line(V2i(3, 2), V2i(gs.0 - 1, gs.1 / 2)) {
rg.get_mut(pt).content = Content::Air;
}
}
#[test]
fn render_default_region() -> io::Result<()> {
let rs = make_test_renderer()?;
let rg = make_test_region();
let mut out = io::stdout();
rs.render(&rg, &mut BasicRenderer, &mut out)
}
#[test]
fn render_full_region() -> io::Result<()> {
let rs = make_test_renderer()?;
let mut rg = make_test_region();
for pt in &R2i::origin_dim(V2i(0, 0), rg.grid_size()) {
rg.get_mut(pt).content = Content::Air;
}
let mut out = io::stdout();
rs.render(&rg, &mut BasicRenderer, &mut out)
}
#[test]
fn render_line_region_basic() -> io::Result<()> {
let rs = make_test_renderer()?;
let mut rg = make_test_region();
draw_line(&mut rg);
let mut out = io::stdout();
rs.render(&rg, &mut BasicRenderer, &mut out)
}
#[test]
fn render_line_region_edge() -> io::Result<()> {
let rs = make_test_renderer()?;
let mut rg = make_test_region();
draw_line(&mut rg);
let mut out = io::stdout();
rs.render(&rg, &mut EdgeRenderer, &mut out)
}
#[test]
fn render_line_region_invert() -> io::Result<()> {
let rs = make_test_renderer()?;
let mut rg = make_test_region();
draw_line(&mut rg);
let mut out = io::stdout();
rs.render(&rg, &mut InvertRenderer, &mut out)
}
}
Loading…
Cancel
Save