Skip to main content

hexroll3_scroll/
instance.rs

1/*
2// Copyright (C) 2020-2025 Pen, Dice & Paper
3//
4// This program is dual-licensed under the following terms:
5//
6// Option 1: (Non-Commercial) GNU Affero General Public License (AGPL)
7// This program is free software: you can redistribute it and/or modify
8// it under the terms of the GNU Affero General Public License as
9// published by the Free Software Foundation, either version 3 of the
10// License, or (at your option) any later version.
11//
12// This program is distributed in the hope that it will be useful,
13// but WITHOUT ANY WARRANTY; without even the implied warranty of
14// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15// GNU Affero General Public License for more details.
16//
17// You should have received a copy of the GNU Affero General Public License
18// along with this program. If not, see <http://www.gnu.org/licenses/>.
19//
20// Option 2: Commercial License
21// For commercial use, you are required to obtain a separate commercial
22// license. Please contact ithai at pendicepaper.com
23// for more information about commercial licensing terms.
24*/
25use std::cell::RefCell;
26use std::collections::HashMap;
27use std::path::PathBuf; // Trait that provides the `choose` method
28
29use anyhow::anyhow;
30use anyhow::Result;
31use minijinja::Environment;
32use rand::distributions::Alphanumeric;
33use rand::rngs::ThreadRng;
34use rand::seq::SliceRandom;
35use rand::thread_rng;
36use rand::Rng;
37
38use crate::generators::roll;
39use crate::parser::parse_buffer;
40use crate::parser::parse_file;
41use crate::renderer_env::prepare_renderer;
42use crate::repository::*;
43use crate::semantics::*;
44
45/// SandboxBuilder is a wrapper for sandbox instances, providing the
46/// additional facilities required to generate content.
47pub struct SandboxBuilder<'a> {
48    pub sandbox: &'a SandboxInstance,
49    pub randomizer: Randomizer,
50    pub templating_env: Environment<'a>,
51}
52
53impl<'a> SandboxBuilder<'a> {
54    pub fn from_instance(instance: &'a SandboxInstance) -> Self {
55        let mut env = Environment::new();
56        prepare_renderer(&mut env, instance);
57        SandboxBuilder {
58            sandbox: instance,
59            randomizer: Randomizer::new(),
60            templating_env: env,
61        }
62    }
63}
64
65/// SandboxInstance holds all the data needed to read and render
66/// generated content as well as the model for generating content.
67pub struct SandboxInstance {
68    pub sid: Option<String>,
69    pub classes: HashMap<String, Class>,
70    pub repo: Repository,
71    pub globals: HashMap<String, serde_json::Value>,
72}
73
74impl SandboxInstance {
75    pub fn new() -> Self {
76        SandboxInstance {
77            sid: None,
78            classes: HashMap::new(),
79            repo: Repository::new(),
80            globals: HashMap::new(),
81        }
82    }
83
84    pub fn with_scroll(&mut self, scroll_filepath: PathBuf) -> Result<&mut Self> {
85        parse_file(self, scroll_filepath)?;
86        Ok(self)
87    }
88
89    pub fn open(&mut self, filepath: &str) -> Result<&mut Self> {
90        self.repo.open(filepath)?;
91        let root = self.repo.inspect(|tx| tx.load("root"))?;
92        if let Some(sid) = root.value.as_str() {
93            self.sid = Some(sid.to_string());
94            Ok(self)
95        } else {
96            Err(anyhow!("Unable to find root entity in {}", filepath))
97        }
98    }
99
100    pub fn create(&mut self, filepath: &str) -> Result<&mut Self> {
101        self.repo.create(filepath)?;
102
103        if let Ok(sid) = self.repo.mutate(|tx| {
104            let builder = SandboxBuilder::from_instance(self);
105            let ret = roll(&builder, tx, "main", "root", None);
106            tx.store("root", &serde_json::json!(ret.as_ref().unwrap()))?;
107            ret
108        }) {
109            self.sid = Some(sid.to_string());
110            Ok(self)
111        } else {
112            Err(anyhow!(
113                "Was unable to create a new sandbox in {}",
114                filepath
115            ))
116        }
117    }
118
119    pub fn sid(&self) -> Option<String> {
120        self.sid.clone()
121    }
122
123    pub fn parse_buffer(&mut self, buffer: &str) -> &mut Self {
124        parse_buffer(self, buffer, None, None).unwrap();
125        self
126    }
127}
128
129impl Default for SandboxInstance {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135pub struct Randomizer {
136    rng: RefCell<ThreadRng>, // Use RefCell for interior mutability
137}
138
139impl Randomizer {
140    pub fn new() -> Self {
141        Randomizer {
142            rng: RefCell::new(thread_rng()),
143        }
144    }
145
146    pub fn choose<'a, T>(&self, v: &'a Vec<T>) -> &'a T {
147        match v.choose(&mut *self.rng.borrow_mut()) {
148            Some(item) => item,
149            None => panic!("List is empty"),
150        }
151    }
152
153    pub fn uid(&self) -> String {
154        let mut rng = self.rng.borrow_mut();
155        (0..8).map(|_| rng.sample(Alphanumeric) as char).collect()
156    }
157
158    pub fn in_range(&self, min: i32, max: i32) -> i32 {
159        let mut rng = self.rng.borrow_mut();
160        rng.gen_range(min..max + 1)
161    }
162}
163
164impl Default for Randomizer {
165    fn default() -> Self {
166        Self::new()
167    }
168}