beginnings of information strategy
This commit is contained in:
parent
f09dd58cda
commit
efba24d6e8
6 changed files with 451 additions and 44 deletions
|
@ -65,7 +65,7 @@ impl CardCounts {
|
|||
get_count_for_value(&card.value) - count
|
||||
}
|
||||
|
||||
pub fn add(&mut self, card: &Card) {
|
||||
pub fn increment(&mut self, card: &Card) {
|
||||
let count = self.counts.get_mut(card).unwrap();
|
||||
*count += 1;
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ impl Discard {
|
|||
}
|
||||
|
||||
pub fn place(&mut self, card: Card) {
|
||||
self.counts.add(&card);
|
||||
self.counts.increment(&card);
|
||||
self.cards.push(card);
|
||||
}
|
||||
}
|
||||
|
|
30
src/game.rs
30
src/game.rs
|
@ -33,8 +33,8 @@ pub struct Hint {
|
|||
#[derive(Debug,Clone)]
|
||||
pub enum TurnChoice {
|
||||
Hint(Hint),
|
||||
Discard(usize),
|
||||
Play(usize),
|
||||
Discard(usize), // index of card to discard
|
||||
Play(usize), // index of card to play
|
||||
}
|
||||
|
||||
// represents what happened in a turn
|
||||
|
@ -292,33 +292,19 @@ impl BoardState {
|
|||
}
|
||||
}
|
||||
|
||||
fn probability_of_predicate<T>(
|
||||
&self,
|
||||
card_info: &T,
|
||||
predicate: &Fn(&Self, &Card) -> bool
|
||||
) -> f32 where T: CardInfo {
|
||||
let mut total_weight = 0;
|
||||
let mut playable_weight = 0;
|
||||
for card in card_info.get_possibilities() {
|
||||
let weight = card_info.get_weight(&card);
|
||||
if predicate(&self, &card) {
|
||||
playable_weight += weight;
|
||||
}
|
||||
total_weight += weight;
|
||||
}
|
||||
(playable_weight as f32) / (total_weight as f32)
|
||||
}
|
||||
|
||||
pub fn probability_is_playable<T>(&self, card_info: &T) -> f32 where T: CardInfo {
|
||||
self.probability_of_predicate(card_info, &Self::is_playable)
|
||||
let f = |card: &Card| { self.is_playable(card) };
|
||||
card_info.probability_of_predicate(&f)
|
||||
}
|
||||
|
||||
pub fn probability_is_dead<T>(&self, card_info: &T) -> f32 where T: CardInfo {
|
||||
self.probability_of_predicate(card_info, &Self::is_dead)
|
||||
let f = |card: &Card| { self.is_dead(card) };
|
||||
card_info.probability_of_predicate(&f)
|
||||
}
|
||||
|
||||
pub fn probability_is_dispensable<T>(&self, card_info: &T) -> f32 where T: CardInfo {
|
||||
self.probability_of_predicate(card_info, &Self::is_dispensable)
|
||||
let f = |card: &Card| { self.is_dispensable(card) };
|
||||
card_info.probability_of_predicate(&f)
|
||||
}
|
||||
|
||||
pub fn get_players(&self) -> Vec<Player> {
|
||||
|
|
78
src/info.rs
78
src/info.rs
|
@ -2,6 +2,7 @@ use std::cmp::Eq;
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
use std::convert::From;
|
||||
|
||||
use cards::*;
|
||||
|
||||
|
@ -20,8 +21,6 @@ pub trait CardInfo {
|
|||
// whether the card is possible
|
||||
fn is_possible(&self, card: &Card) -> bool;
|
||||
|
||||
// TODO: have a borrow_possibilities to allow for more efficiency?
|
||||
|
||||
// mark all current possibilities for the card
|
||||
fn get_possibilities(&self) -> Vec<Card> {
|
||||
let mut v = Vec::new();
|
||||
|
@ -37,10 +36,10 @@ pub trait CardInfo {
|
|||
}
|
||||
// get probability weight for the card
|
||||
#[allow(unused_variables)]
|
||||
fn get_weight(&self, card: &Card) -> u32 {
|
||||
1
|
||||
fn get_weight(&self, card: &Card) -> f32 {
|
||||
1 as f32
|
||||
}
|
||||
fn get_weighted_possibilities(&self) -> Vec<(Card, u32)> {
|
||||
fn get_weighted_possibilities(&self) -> Vec<(Card, f32)> {
|
||||
let mut v = Vec::new();
|
||||
for card in self.get_possibilities() {
|
||||
let weight = self.get_weight(&card);
|
||||
|
@ -48,6 +47,25 @@ pub trait CardInfo {
|
|||
}
|
||||
v
|
||||
}
|
||||
fn weighted_score<T>(&self, score_fn: &Fn(&Card) -> T) -> f32
|
||||
where f32: From<T>
|
||||
{
|
||||
let mut total_score = 0.;
|
||||
let mut total_weight = 0.;
|
||||
for card in self.get_possibilities() {
|
||||
let weight = self.get_weight(&card);
|
||||
let score = f32::from(score_fn(&card));
|
||||
total_weight += weight;
|
||||
total_score += weight * score;
|
||||
}
|
||||
total_score / total_weight
|
||||
}
|
||||
fn probability_of_predicate(&self, predicate: &Fn(&Card) -> bool) -> f32 {
|
||||
let f = |card: &Card| {
|
||||
if predicate(card) { 1.0 } else { 0.0 }
|
||||
};
|
||||
self.weighted_score(&f)
|
||||
}
|
||||
|
||||
// mark a whole color as false
|
||||
fn mark_color_false(&mut self, color: &Color);
|
||||
|
@ -215,31 +233,53 @@ impl fmt::Display for SimpleCardInfo {
|
|||
|
||||
// Can represent information of the form:
|
||||
// this card is/isn't possible
|
||||
// also, maintains weights for the cards
|
||||
// also, maintains integer weights for the cards
|
||||
#[derive(Clone)]
|
||||
pub struct CardPossibilityTable {
|
||||
possible: HashMap<Card, u32>,
|
||||
}
|
||||
impl CardPossibilityTable {
|
||||
pub fn new() -> CardPossibilityTable {
|
||||
Self::from(&CardCounts::new())
|
||||
}
|
||||
|
||||
// mark a possible card as false
|
||||
pub fn mark_false(&mut self, card: &Card) {
|
||||
self.possible.remove(card);
|
||||
}
|
||||
|
||||
// a bit more efficient
|
||||
pub fn borrow_possibilities<'a>(&'a self) -> Vec<&'a Card> {
|
||||
self.possible.keys().collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn decrement_weight_if_possible(&mut self, card: &Card) {
|
||||
if self.is_possible(card) {
|
||||
self.decrement_weight(card);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decrement_weight(&mut self, card: &Card) {
|
||||
let weight =
|
||||
self.possible.get_mut(card)
|
||||
.expect(&format!("Decrementing weight for impossible card: {}", card));
|
||||
*weight -= 1;
|
||||
}
|
||||
}
|
||||
impl <'a> From<&'a CardCounts> for CardPossibilityTable {
|
||||
fn from(counts: &'a CardCounts) -> CardPossibilityTable {
|
||||
let mut possible = HashMap::new();
|
||||
for &color in COLORS.iter() {
|
||||
for &value in VALUES.iter() {
|
||||
possible.insert(
|
||||
Card::new(color, value),
|
||||
get_count_for_value(&value)
|
||||
);
|
||||
let card = Card::new(color, value);
|
||||
let count = counts.remaining(&card);
|
||||
possible.insert(card, count);
|
||||
}
|
||||
}
|
||||
CardPossibilityTable {
|
||||
possible: possible,
|
||||
}
|
||||
}
|
||||
|
||||
// mark a possible card as false
|
||||
fn mark_false(&mut self, card: &Card) {
|
||||
self.possible.remove(card);
|
||||
}
|
||||
}
|
||||
impl CardInfo for CardPossibilityTable {
|
||||
fn is_possible(&self, card: &Card) -> bool {
|
||||
|
@ -261,14 +301,14 @@ impl CardInfo for CardPossibilityTable {
|
|||
self.mark_false(&Card::new(color, value.clone()));
|
||||
}
|
||||
}
|
||||
fn get_weight(&self, card: &Card) -> u32 {
|
||||
*self.possible.get(card).unwrap_or(&0)
|
||||
fn get_weight(&self, card: &Card) -> f32 {
|
||||
*self.possible.get(card).unwrap_or(&0) as f32
|
||||
}
|
||||
}
|
||||
impl fmt::Display for CardPossibilityTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for card in self.get_possibilities() {
|
||||
try!(f.write_str(&format!("{}, ", card)));
|
||||
for (card, weight) in &self.possible {
|
||||
try!(f.write_str(&format!("{} {}, ", weight, card)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ mod simulator;
|
|||
mod strategies {
|
||||
pub mod examples;
|
||||
pub mod cheating;
|
||||
pub mod information;
|
||||
}
|
||||
|
||||
use getopts::Options;
|
||||
|
@ -128,6 +129,10 @@ fn main() {
|
|||
Box::new(strategies::cheating::CheatingStrategyConfig::new())
|
||||
as Box<simulator::GameStrategyConfig + Sync>
|
||||
},
|
||||
"info" => {
|
||||
Box::new(strategies::information::InformationStrategyConfig::new())
|
||||
as Box<simulator::GameStrategyConfig + Sync>
|
||||
},
|
||||
_ => {
|
||||
print_usage(&program, opts);
|
||||
panic!("Unexpected strategy argument {}", strategy_str);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::rc::Rc;
|
||||
use std::cell::{RefCell};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use simulator::*;
|
||||
use game::*;
|
||||
|
|
376
src/strategies/information.rs
Normal file
376
src/strategies/information.rs
Normal file
|
@ -0,0 +1,376 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
use rand::{self, Rng};
|
||||
|
||||
use simulator::*;
|
||||
use game::*;
|
||||
|
||||
// strategy that recommends other players an action.
|
||||
//
|
||||
// 50 cards, 25 plays, 25 left
|
||||
// with 5 players:
|
||||
// - only 5 + 8 hints total. each player goes through 10 cards
|
||||
// with 4 players:
|
||||
// - only 9 + 8 hints total. each player goes through 12.5 cards
|
||||
//
|
||||
// For any given player with at least 4 cards, and index i, there are at least 3 hints that can be given.
|
||||
// 1. a value hint on card i
|
||||
// 2. a color hint on card i
|
||||
// 3. any hint not involving card i
|
||||
//
|
||||
// for 4 players, can give 6 distinct hints
|
||||
|
||||
struct ModulusInformation {
|
||||
modulus: u32,
|
||||
value: u32,
|
||||
}
|
||||
|
||||
enum Question {
|
||||
IsPlayable(usize),
|
||||
IsDead(usize),
|
||||
}
|
||||
|
||||
fn answer_question(question: Question, hand: &Cards, view: &GameStateView) -> ModulusInformation {
|
||||
match question {
|
||||
Question::IsPlayable(index) => {
|
||||
let ref card = hand[index];
|
||||
ModulusInformation {
|
||||
modulus: 2,
|
||||
value: if view.board.is_playable(card) { 1 } else { 0 },
|
||||
}
|
||||
},
|
||||
Question::IsDead(index) => {
|
||||
let ref card = hand[index];
|
||||
ModulusInformation {
|
||||
modulus: 2,
|
||||
value: if view.board.is_dead(card) { 1 } else { 0 },
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct InformationStrategyConfig;
|
||||
|
||||
impl InformationStrategyConfig {
|
||||
pub fn new() -> InformationStrategyConfig {
|
||||
InformationStrategyConfig
|
||||
}
|
||||
}
|
||||
impl GameStrategyConfig for InformationStrategyConfig {
|
||||
fn initialize(&self, opts: &GameOptions) -> Box<GameStrategy> {
|
||||
if opts.num_players < 4 {
|
||||
panic!("Information strategy doesn't work with less than 4 players");
|
||||
}
|
||||
Box::new(InformationStrategy::new())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InformationStrategy;
|
||||
|
||||
impl InformationStrategy {
|
||||
pub fn new() -> InformationStrategy {
|
||||
InformationStrategy
|
||||
}
|
||||
}
|
||||
impl GameStrategy for InformationStrategy {
|
||||
fn initialize(&self, player: Player, view: &GameStateView) -> Box<PlayerStrategy> {
|
||||
let mut public_info = HashMap::new();
|
||||
for player in view.board.get_players() {
|
||||
let hand_info = (0..view.board.hand_size).map(|_| { CardPossibilityTable::new() }).collect::<Vec<_>>();
|
||||
public_info.insert(player, hand_info);
|
||||
}
|
||||
Box::new(InformationPlayerStrategy {
|
||||
me: player,
|
||||
public_info: public_info,
|
||||
public_counts: CardCounts::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InformationPlayerStrategy {
|
||||
me: Player,
|
||||
public_info: HashMap<Player, Vec<CardPossibilityTable>>,
|
||||
public_counts: CardCounts, // what any newly drawn card should be
|
||||
}
|
||||
impl InformationPlayerStrategy {
|
||||
// given a hand of cards, represents how badly it will need to play things
|
||||
fn hand_play_value(&self, view: &GameStateView, hand: &Cards/*, all_viewable: HashMap<Color, <Value, usize>> */) -> u32 {
|
||||
// dead = 0 points
|
||||
// indispensible = 5 + (5 - value) points
|
||||
// playable = 1 point
|
||||
let mut value = 0;
|
||||
for card in hand {
|
||||
if view.board.is_dead(card) {
|
||||
continue
|
||||
}
|
||||
if !view.board.is_dispensable(card) {
|
||||
value += 10 - card.value;
|
||||
} else {
|
||||
value += 1;
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn estimate_hand_play_value(&self, view: &GameStateView) -> u32 {
|
||||
0
|
||||
}
|
||||
|
||||
// how badly do we need to play a particular card
|
||||
fn get_average_play_score(&self, view: &GameStateView, card_table: &CardPossibilityTable) -> f32 {
|
||||
let f = |card: &Card| {
|
||||
self.get_play_score(view, card) as f32
|
||||
};
|
||||
card_table.weighted_score(&f)
|
||||
}
|
||||
|
||||
fn get_play_score(&self, view: &GameStateView, card: &Card) -> i32 {
|
||||
let my_hand_value = self.estimate_hand_play_value(view);
|
||||
|
||||
for player in view.board.get_players() {
|
||||
if player != self.me {
|
||||
if view.has_card(&player, card) {
|
||||
let their_hand_value = self.hand_play_value(view, view.get_hand(&player));
|
||||
// they can play this card, and have less urgent plays than i do
|
||||
if their_hand_value <= my_hand_value {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// there are no hints
|
||||
// maybe value 5s more?
|
||||
5 + (5 - (card.value as i32))
|
||||
}
|
||||
|
||||
fn find_useless_card(&self, view: &GameStateView, hand: &Cards) -> Option<usize> {
|
||||
let mut set: HashSet<Card> = HashSet::new();
|
||||
|
||||
for (i, card) in hand.iter().enumerate() {
|
||||
if view.board.is_dead(card) {
|
||||
return Some(i);
|
||||
}
|
||||
if set.contains(card) {
|
||||
// found a duplicate card
|
||||
return Some(i);
|
||||
}
|
||||
set.insert(card.clone());
|
||||
}
|
||||
return None
|
||||
}
|
||||
|
||||
fn someone_else_can_play(&self, view: &GameStateView) -> bool {
|
||||
for player in view.board.get_players() {
|
||||
if player != self.me {
|
||||
for card in view.get_hand(&player) {
|
||||
if view.board.is_playable(card) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_player_public_info(&self, player: &Player) -> &Vec<CardPossibilityTable> {
|
||||
self.public_info.get(player).unwrap()
|
||||
}
|
||||
|
||||
fn get_player_public_info_mut(&mut self, player: &Player) -> &mut Vec<CardPossibilityTable> {
|
||||
self.public_info.get_mut(player).unwrap()
|
||||
}
|
||||
|
||||
fn update_public_info_for_hint(&mut self, hint: &Hint, matches: &Vec<bool>) {
|
||||
let mut info = self.get_player_public_info_mut(&hint.player);
|
||||
let zip_iter = info.iter_mut().zip(matches);
|
||||
match hint.hinted {
|
||||
Hinted::Color(ref color) => {
|
||||
for (card_info, matched) in zip_iter {
|
||||
card_info.mark_color(color, *matched);
|
||||
}
|
||||
}
|
||||
Hinted::Value(ref value) => {
|
||||
for (card_info, matched) in zip_iter {
|
||||
card_info.mark_value(value, *matched);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn update_public_info_for_discard_or_play(
|
||||
&mut self,
|
||||
view: &GameStateView,
|
||||
player: &Player,
|
||||
index: usize,
|
||||
card: &Card
|
||||
) {
|
||||
let new_card_table = CardPossibilityTable::from(&self.public_counts);
|
||||
{
|
||||
let mut info = self.get_player_public_info_mut(&player);
|
||||
assert!(info[index].is_possible(card));
|
||||
info.remove(index);
|
||||
|
||||
// push *before* incrementing public counts
|
||||
if info.len() < view.info.len() {
|
||||
info.push(new_card_table);
|
||||
}
|
||||
}
|
||||
|
||||
// note: other_player could be player, as well
|
||||
// in particular, we will decrement the newly drawn card
|
||||
for other_player in view.board.get_players() {
|
||||
let mut info = self.get_player_public_info_mut(&other_player);
|
||||
for card_table in info {
|
||||
card_table.decrement_weight_if_possible(card);
|
||||
}
|
||||
}
|
||||
|
||||
self.public_counts.increment(card);
|
||||
}
|
||||
|
||||
fn get_private_info(&self, view: &GameStateView) -> Vec<CardPossibilityTable> {
|
||||
let mut info = self.get_player_public_info(&self.me).clone();
|
||||
for card_table in info.iter_mut() {
|
||||
for (other_player, state) in &view.other_player_states {
|
||||
for card in &state.hand {
|
||||
card_table.decrement_weight_if_possible(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
}
|
||||
impl PlayerStrategy for InformationPlayerStrategy {
|
||||
fn decide(&mut self, view: &GameStateView) -> TurnChoice {
|
||||
let private_info = self.get_private_info(view);
|
||||
// debug!("My info:");
|
||||
// for (i, card_table) in private_info.iter().enumerate() {
|
||||
// debug!("{}: {}", i, card_table);
|
||||
// }
|
||||
|
||||
let playable_cards = private_info.iter().enumerate().filter(|&(_, card_table)| {
|
||||
view.board.probability_is_playable(card_table) == 1.0
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
if playable_cards.len() > 0 {
|
||||
// play the best playable card
|
||||
// the higher the play_score, the better to play
|
||||
let mut play_score = -1.0;
|
||||
let mut play_index = 0;
|
||||
|
||||
for (index, card_table) in playable_cards {
|
||||
let score = self.get_average_play_score(view, card_table);
|
||||
if score > play_score {
|
||||
play_score = score;
|
||||
play_index = index;
|
||||
}
|
||||
}
|
||||
|
||||
TurnChoice::Play(play_index)
|
||||
} else {
|
||||
if view.board.hints_remaining > 0 {
|
||||
let hint_player = view.board.player_to_left(&self.me);
|
||||
let hint_card = rand::thread_rng().choose(&view.get_hand(&hint_player)).unwrap();
|
||||
let hinted = {
|
||||
if rand::random() {
|
||||
// hint a color
|
||||
Hinted::Color(hint_card.color)
|
||||
} else {
|
||||
Hinted::Value(hint_card.value)
|
||||
}
|
||||
};
|
||||
TurnChoice::Hint(Hint {
|
||||
player: hint_player,
|
||||
hinted: hinted,
|
||||
})
|
||||
} else {
|
||||
TurnChoice::Discard(0)
|
||||
}
|
||||
}
|
||||
|
||||
// // 50 total, 25 to play, 20 in hand
|
||||
// if view.board.discard.cards.len() < 6 {
|
||||
// // if anything is totally useless, discard it
|
||||
// if let Some(i) = self.find_useless_card(view) {
|
||||
// return TurnChoice::Discard(i);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // hinting is better than discarding dead cards
|
||||
// // (probably because it stalls the deck-drawing).
|
||||
// if view.board.hints_remaining > 1 {
|
||||
// if self.someone_else_can_play(view) {
|
||||
// return self.throwaway_hint(view);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // if anything is totally useless, discard it
|
||||
// if let Some(i) = self.find_useless_card(view) {
|
||||
// return TurnChoice::Discard(i);
|
||||
// }
|
||||
|
||||
// // All cards are plausibly useful.
|
||||
// // Play the best discardable card, according to the ordering induced by comparing
|
||||
// // (is in another hand, is dispensable, value)
|
||||
// // The higher, the better to discard
|
||||
// let mut discard_card = None;
|
||||
// let mut compval = (false, false, 0);
|
||||
// for card in my_cards {
|
||||
// let my_compval = (
|
||||
// view.can_see(card),
|
||||
// view.board.is_dispensable(card),
|
||||
// card.value,
|
||||
// );
|
||||
// if my_compval > compval {
|
||||
// discard_card = Some(card);
|
||||
// compval = my_compval;
|
||||
// }
|
||||
// }
|
||||
// if let Some(card) = discard_card {
|
||||
// if view.board.hints_remaining > 0 {
|
||||
// if !view.can_see(card) {
|
||||
// return self.throwaway_hint(view);
|
||||
// }
|
||||
// }
|
||||
|
||||
// let index = my_cards.iter().position(|iter_card| {
|
||||
// card == iter_card
|
||||
// }).unwrap();
|
||||
// TurnChoice::Discard(index)
|
||||
// } else {
|
||||
// panic!("This shouldn't happen! No discardable card");
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn update(&mut self, turn: &Turn, view: &GameStateView) {
|
||||
match turn.choice {
|
||||
TurnChoice::Hint(ref hint) => {
|
||||
if let &TurnResult::Hint(ref matches) = &turn.result {
|
||||
self.update_public_info_for_hint(hint, matches);
|
||||
} else {
|
||||
panic!("Got turn choice {:?}, but turn result {:?}",
|
||||
turn.choice, turn.result);
|
||||
}
|
||||
}
|
||||
TurnChoice::Discard(index) => {
|
||||
if let &TurnResult::Discard(ref card) = &turn.result {
|
||||
self.update_public_info_for_discard_or_play(view, &turn.player, index, card);
|
||||
} else {
|
||||
panic!("Got turn choice {:?}, but turn result {:?}",
|
||||
turn.choice, turn.result);
|
||||
}
|
||||
}
|
||||
TurnChoice::Play(index) => {
|
||||
if let &TurnResult::Play(ref card, played) = &turn.result {
|
||||
self.update_public_info_for_discard_or_play(view, &turn.player, index, card);
|
||||
} else {
|
||||
panic!("Got turn choice {:?}, but turn result {:?}",
|
||||
turn.choice, turn.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue