#!/usr/bin/env python3
# coding: utf-8

import numpy as np
import pencil as pc
from scipy.optimize import curve_fit
from helperfuncs import *
import matplotlib as mpl
mpl.use('pgf')
import sys
from os import listdir, path
from mpl_toolkits.axes_grid1 import Grid
import argparse
from os import mkdir, path, listdir
from os.path import join, isdir, isfile

parser = argparse.ArgumentParser()
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='add verbosity')
parser.add_argument('ddir', default='', type=str, help='datadir directory')
parser.add_argument('choice', default='', type=str, choices=('taylor', 'energy'), help='choice of plotting')

args = parser.parse_args()
figdir = 'figures'

dstart = args.ddir.split('_')[0]
dirs = [l for l in listdir('.') if l.startswith(dstart) and isdir(l)]
try:
    dirs = sorted(dirs, key=lambda a: float(a.split('_')[-1]))
except ValueError:
    dirs = sorted(dirs, key=lambda a: float(a.split('_')[-1][1:]))
dirs.append('helical')
if args.verbose: print(dirs)

#if args.choice == 'energy':
#    print('energy plot not yet implemented, choose taylor instead')
#    exit(1)

def figsize(scale, ratio=None):
    #fig_width_pt = 418.25555
    fig_width_pt = 523.5307              # Get this from LaTeX using \the\textwidth
    inches_per_pt = 1.0/72.27                       # Convert pt to inch
    fig_width = fig_width_pt*inches_per_pt*scale    # width in inches
    if not ratio:
        golden_mean = (np.sqrt(5.0)-1.0)/2.0   #golden_mean
        ratio = golden_mean
    fig_height = fig_width*ratio             # height in inches
    fig_size = [fig_width,fig_height]
    return fig_size

# I make my own newfig and savefig functions
def newfig(width):
#    plt.clf()
    fig = plt.figure(figsize=figsize(width, ratio=0.75))
    ax = fig.add_subplot(111)
    return fig, ax

def savefig(fig, filename):
    fig.savefig('{}.pgf'.format(filename))
    fig.savefig('{}.pdf'.format(filename))

def make_tsplot(fig, ax):
    if args.choice == 'taylor':
        try:
            plotarr = 2*np.pi*tser.brms/tser.jrms
        except AttributeError:
            return
    elif args.choice == 'energy':
        plotarr = tser.brms**2
    ax.plot(tser.t, plotarr, label=dd.split('_')[-1], linewidth=1.5,
            color=plt.cm.Paired(next(clrindx)))
    ti = search_indx(tser.t, 1., eps=.05)
    ti2 = search_indx(tser.t, 0.8*tser.t[-1], eps=.05)
    if args.verbose: print('index from which fitting begins: ',ti)
    if ti is not None:
        po1, pco1 = curve_fit(lambda x, a, b: powerlaw(x, a, b, x0=0), tser.t[ti:], plotarr[ti:])
        #ax.plot(tser.t[ti:ti2], powerlaw(tser.t[ti:ti2],1.2*po1[0],po1[1]), linewidth=1.5,
        #        label=r'fit: $B\sim t^{%.1f}$' % po1[1], color=(0.72, 0.61, 0.46, 1.00))
        if args.verbose:
            print(dd, 'L ~ t^{1:.2f}'.format(*po1))

def setup_tsplot(ax):
    ax.set_yscale('log')
    ax.set_xscale('log')
    ax.set_xlabel('time $t$')
    ax.set_ylabel(r'$L_{\textrm{T}}$')

pgf_with_latex = {                      # setup matplotlib to use latex for output
    "pgf.texsystem": "pdflatex",        # change this if using xetex or lautex
    "text.usetex": True,                # use LaTeX to write all tex
    "font.family": "serif",
    "font.serif": [],                   # blank entries should cause plots to inherit fonts from the document
    "font.sans-serif": [],
    "font.monospace": [],
    "font.size": 10,
    "axes.labelsize": 10,               # LaTeX default is 10pt font.
    "legend.fontsize": 8,               # Make the legend/label fonts a little smaller
    "xtick.labelsize": 8,
    "ytick.labelsize": 8,
    "figure.figsize": figsize(0.95),     # default fig size of 0.9 textwidth
    "pgf.preamble": [
        r"\usepackage[utf8x]{inputenc}",    # use utf8 fonts becasue your computer can handle it :)
        r"\usepackage[T1]{fontenc}",        # plots will be generated using this preamble
        ]
    }
mpl.rcParams.update(pgf_with_latex)

import matplotlib.pyplot as plt

# Setup Figure and Axes

fig, ax  = newfig(0.45)
setup_tsplot(ax)#, xlim=(.1,tser.t[-1]))
tmax = 0.
clrindx = iter(np.linspace(0,1,len(dirs)))

for dd in dirs:
    if args.verbose: print(dd)
    tser = pc.read_ts(datadir=dd, quiet=not args.verbose)
    make_tsplot(fig, ax)
    if tser.t[-1] > tmax:
        ax.set_xlim(.1, tser.t[-1])
    ax.legend(loc='upper left', frameon=False)

filename = dstart + '_taylor'
fig.tight_layout(pad=0.3)
if args.verbose:
    print(filename)
savefig(fig, path.join(figdir, filename))
print('SUCCESS')


# Given a non-negative number "num", return True if num is within 2 of a multiple of 10.

def near_ten(num):
    remainder = num % 10
    result = True if remainder <= 2 or  abs(10 - remainder) <= 2 else False
    print(f"near_ten({num}) -> {result}")

near_ten(12)
near_ten(17)
near_ten(19)


'''
	Python module to compute information theoretical quantities
'''

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import scipy.spatial as ss
from math import log,pi,exp
from sklearn.neighbors import NearestNeighbors

def BinNeuronEntropy(SpikeTrain):
	r'''
	Description: Computes the entropy of a binary neuron (with a response consisting of 0s and 1s).
	Inputs:
	SpikeTrain: Binary spike train of a neuron (must be composed of 0s and 1s)
	Outputs:
	Returns the entropy of the binary spike train
	'''
	T = len(SpikeTrain) # Length of the spike train
	P_firing = np.sum(SpikeTrain) / float(T) # Probability of firing (probability of the spike trai being 1)
	P_notfiring = 1.0 - P_firing # Pobability of silence (probability of the spike tain being 0)
	# Computing and returning the entropy of the binary spike train
	return -P_firing*np.log2(P_firing) - P_notfiring*np.log2(P_notfiring)

def BinNeuronEntropy_2(Sx, Sy):
	r'''
	Description: Returns the joint entropy of the binary spike trains Sx and Sy.
	Sx, Sy: Binary spike train of a neuron (must be composed of 0s and 1s)
	Outputs:
	Returns the joint entropy H(X,Y) of the binary spike train
	'''
	N = len(Sx)   # Number of bins in the spike train
	PXY = np.zeros([2,2])
	for t in range(len(Sx)):
		i, j     = Sx[t], Sy[t]
		if i == 0 and j == 0:
			continue
		else:
			PXY[i,j] += 1
	# Estimating [0, 0] pairs for the PXY matrix
	PXY[0,0] = N - PXY.sum()
	# Normalizing probabilities
	PXY = PXY / N
	# Computing joint entropy
	HXY = 0
	for p in PXY.flatten('C'):
		if p > 0.00001:
			HXY -= p*np.log2(p)
		else:
			continue
	return HXY

def BinNeuronEntropy_3(Sx, Sy, Sz):
	r'''
	Description: Returns the joint entropy of the binary spike trains Sx, Sy and Sy.
	Sx, Sy, Sz: Binary spike train of a neuron (must be composed of 0s and 1s)
	Outputs:
	Returns the joint entropy H(X,Y) of the binary spike train
	'''
	N = len(Sx)   # Number of bins in the spike train
	PXYZ = np.zeros([2,2,2])
	for t in range(len(Sx)):
		i, j, k = Sx[t], Sy[t], Sz[t]
		if i == 0 and j == 0 and k == 0:
			continue
		else:
			PXYZ[i,j,k] += 1
	# Estimating [0, 0, 0] pairs for the PXYZ matrix
	PXYZ[0,0,0] = N - PXYZ.sum()
	# Normalizing probabilities
	PXYZ = PXYZ / N
	# Computing joint entropy
	HXYZ = 0
	for p in PXYZ.flatten('C'):
		if p > 0.00001:
			HXYZ -= p*np.log2(p)
		else:
			continue
	return HXYZ

def BinNeuronConditionalMI(Sx, Sy, Sz):
	r'''
	Description: Returns the conditional mutual information. Sx, SY conditioned in Sz.
	Sx, Sy, Sz: Binary spike train of a neuron (must be composed of 0s and 1s)
	Outputs:
	Returns the joint entropy H(X,Y) of the binary spike train
	'''
	HXZ  = BinNeuronEntropy_2(Sx, Sz)
	HYZ  = BinNeuronEntropy_2(Sy, Sz)
	HXYZ = BinNeuronEntropy_3(Sx, Sy, Sz)
	HZ   = BinNeuronEntropy(Sz)

	return HXZ + HYZ - HXYZ - HZ

def EntropyFromProbabilities(Prob):
	r'''
	Description: Computes the entropy of given probability distribution.
	Inputs:
	Prob: Probability distribution of a random variable
	Outputs:
	H: Entropy of the probabilitie distribution.
	'''
	H = 0
	s = Prob.shape
	for p in Prob:
		if p > 1e-10:
			H -= p*np.log2(p)
	return H

def binMutualInformation(sX, sY, tau):
	r'''
	Description: Computes the delayed mutual information bewtween two binary spike trains.
	Inputs:
	sX: Binary spike train of neuron X
	sY: Binary spike train of neuron Y
	tau: Delay applied in the spike train of neuron Y
	Outputs:
	MI: Returns the mutual information MI(sX, sY)
	'''
	PX = np.zeros([2])
	PY = np.zeros([2])
	PXY = np.zeros([2,2])
	for t in range( np.maximum(0, 0-tau), np.minimum(len(sX)-tau, len(sX)) ):
		PX[1] += sX[t]
		PY[1] += sY[t+tau]
		if (sX[t]==0) and (sY[t+tau]==0):
			continue
		else:
			PXY[sX[t], sY[t+tau]] += 1

	# Estimating [0, 0] pairs for the PXY matrix
	N = len(sX)   # Number of bins in the spike train
	Np = N - tau  # Number of pairs
	PXY[0,0] = Np - np.sum(PXY)
	PX[0]    = Np - PX[1]
	PY[0]    = Np - PY[1]
	# Normalizing probabilities
	PX = PX / np.sum(PX)
	PY = PY / np.sum(PY)
	PXY = PXY / np.sum(PXY)
	HX = EntropyFromProbabilities(PX)
	HY = EntropyFromProbabilities(PY)
	HXY = EntropyFromProbabilities(np.reshape(PXY, (4)))
	MI  = HX + HY - HXY
	return MI

def binTransferEntropy(x, y, delay):
	r'''
	Description: Computes the delayed transfer entropy, from y to x, bewtween two binary spike trains.
	Inputs:
	x: Binary spike train of neuron X
	y: Binary spike train of neuron Y
	delay: Delay applied in the spike train of neuron Y
	Outputs:
	TE: Returns the Transfer Entropy TEy->x
	'''
	T = len(x)

	if delay == 1:
		delay = 0
	elif delay > 1:
		delay -= 1

	px = np.array( [T-np.sum(x), np.sum(x)] ) / float(T) # p(x)
	py = np.array( [T-np.sum(y), np.sum(y)] ) / float(T) # p(y)
	pxy = np.zeros([2, 2])                               # p(x, y)
	pxy1 = np.zeros([2, 2])                              # p(x1, y)
	pxyz = np.zeros([2, 2, 2])                           # p(x1, x, y)

	for i in range(0, T-delay):
		if (x[i+delay] == 0 and y[i] == 0):
			continue
		else:
			pxy[ x[i+delay], y[i] ] += 1.0

	pxy[0,0] = (T - delay - np.sum(pxy))
	pxy = pxy / float(T-delay)

	for i in range(0, T-1):
		if (x[i+1] == 0 and x[i] == 0):
			continue
		else:
			pxy1[ x[i+1], x[i] ] += 1.0

	for i in range(0, T-1-delay):
		if (x[i+1+delay] == 0 and x[i+delay] == 0 and y[i] == 0):
			continue
		else:
			pxyz[ x[i+1+delay], x[i+delay], y[i] ] += 1.0

	pxy1[0,0] = (T - 1 - np.sum(pxy1))
	pxyz[0,0,0] = (T - 1 - delay - np.sum(pxyz))
	# Normalizing probabilities
	pxy1 = pxy1 / float(T-1)
	pxyz = pxyz / float(T-1-delay)

	TE = 0
	for xn in [0, 1]:
		for yn in [0, 1]:
			for xn1 in [0, 1]:
				if pxy1[xn1, xn] > 0.00001 and pxy[xn, yn] > 0.00001 and pxyz[xn1, xn, yn] > 0.00001 and px[xn] > 0.00001:
					TE += pxyz[xn1, xn, yn] * np.log2( ( pxyz[xn1, xn, yn] * px[xn] ) / ( pxy1[xn1, xn] * pxy[xn, yn] ) )
				else:
					continue
	return TE


# Demonstrates using a multiple inputs with a single test function.
import math
import pytest

# Note how it is possible to combine groupings with parametrize.
@pytest.mark.math
@pytest.mark.parametrize('i, o', [(25, 30), (9, 3)])
def test_param_sqrt(input, i, o):
    n = i
    assert math.sqrt(n) + input == o

@pytest.mark.others
@pytest.mark.parametrize('i, o', [(25, 49), (7, 49)])
def test_param_square(i, o):
    n = i
    assert pow(n, 2) == o


from typing import Optional
import torch
import torch.nn as nn

class MaskedLanguageModeling(nn.Module):
    def __init__(self, bert: nn.Module, voc_size:int=30000):
        super(MaskedLanguageModeling, self).__init__()
        self.bert = bert
        d_model = bert.emb.tok_emb.weight.size(1)
        self.linear = nn.Linear(d_model, voc_size)

    def forward(self, input: torch.Tensor, seg: torch.Tensor) -> torch.Tensor:
        '''
        param:
            input: a batch of sequences of words
            seg: Segmentation embedding for input tokens
        dim:
            input:
                input: [B, S]
                seg: [B, S]
            output:
                result: [B, S, V]
        '''
        output = self.bert(input, seg) # [B, S, D_model]
        output = self.linear(output) # [B, S, voc_size]

        return output # [B, S, voc_size]

class NextSentencePrediction(nn.Module):
    def __init__(self, bert: nn.Module):
        super(NextSentencePrediction, self).__init__()
        self.bert = bert
        d_model = bert.emb.tok_emb.weight.size(1)
        self.linear = nn.Linear(d_model, 2)

    def forward(self, input: torch.Tensor, seg: torch.Tensor) -> torch.Tensor:
        '''
        param:
            input: a batch of sequences of words
            seg: Segmentation embedding for input tokens
        dim:
            input:
                input: [B, S]
                seg: [B, S]
            output:
                result: [B, S, V]
        '''
        output = self.bert(input, seg) # [B, S, D_model]
        output = self.linear(output) # [B, S, 2]

        return output[:, 0, :] # [B, 2]

class BertModel(nn.Module):
    def __init__(self, voc_size:int=30000, seq_len: int=512, d_model: int=768, d_ff:int=3072, pad_idx: int=1,
                num_encoder: int=12, num_heads: int=12, dropout: float=0.1):
        super(BertModel, self).__init__()
        self.pad_idx = pad_idx
        self.emb = BERTEmbedding(seq_len, voc_size, d_model, dropout)
        self.encoders = Encoders(seq_len, d_model, d_ff, num_encoder, num_heads, dropout)

    def forward(self, input: torch.Tensor, seg: torch.Tensor) -> torch.Tensor:
        '''
        param:
            input: a batch of sequences of words
        dim:
            input:
                input: [B, S]
            output:
                result: [B, S, V]
        '''
        pad_mask = get_attn_pad_mask(input, input, self.pad_idx)

        emb = self.emb(input, seg) # [B, S, D_model]
        output = self.encoders(emb, pad_mask) # [B, S, D_model]

        return output # [B, S, D_model]

class Encoders(nn.Module):
    def __init__(self, seq_len: int, d_model: int, d_ff:int, num_encoder: int, num_heads: int, dropout: float):
        super(Encoders, self).__init__()
        self.models = nn.ModuleList([Encoder(d_model, d_ff, seq_len, num_heads, dropout=dropout) for i in range(num_encoder)])

    def forward(self, tokens, enc_mask):
        # input: [B, S, D_model]
        for model in self.models:
            tokens = model(tokens, enc_mask)

        return tokens

class PositionalEncoding(nn.Module):
    def __init__(self, d_model: int, seq_len: int, dropout: int=0.1):
        super(PositionalEncoding, self).__init__()
        self.seq_len = seq_len
        self.dropout = nn.Dropout(dropout)
        self.emb = nn.Embedding(seq_len, d_model)

    def forward(self, x: torch.Tensor):
        # x: [B, S]. x is tokens
        pos = torch.arange(self.seq_len, dtype=torch.long, device=x.device) # [S]
        pos = pos.unsqueeze(0).expand(x.size()) # [1, S] -> [B, S]
        pos_emb = self.emb(pos)
        return self.dropout(pos_emb) # [B, S, D_model]

class BERTEmbedding(nn.Module):
    """
    Embeddings for BERT.
    It includes segmentation embedding, token embedding and positional embedding.
    I add dropout for every embedding layer just like the original transformer.
    """
    def __init__(self, seq_len: int=512, voc_size: int=30000, d_model: int=768, dropout: float=0.1) -> None:
        super(BERTEmbedding, self).__init__()
        self.tok_emb = nn.Embedding(num_embeddings=voc_size, embedding_dim=d_model)
        self.tok_dropout = nn.Dropout(dropout)
        self.seg_emb = nn.Embedding(2, d_model)
        self.seg_dropout = nn.Dropout(dropout)
        self.pos_emb = PositionalEncoding(d_model, seq_len, dropout)

    def forward(self, tokens: torch.Tensor, seg: torch.Tensor):
        """
        tokens: [B, S]
        seg: [B, S]. seg is binary tensor. 0 indicates that the corresponding token for its index belongs sentence A
        """
        tok_emb = self.tok_emb(tokens) # [B, S, d_model]
        seg_emb = self.seg_emb(seg) # [B, S, d_model]
        pos_emb = self.pos_emb(tokens) # [B, S, d_model]

        return self.tok_dropout(tok_emb) + self.seg_dropout(seg_emb) + pos_emb  # [B, S, d_model]

class Encoder(nn.Module):
    def __init__(self, d_model: int, d_ff:int, seq_len: int, num_enc_heads: int, dropout: float):
        super(Encoder, self).__init__()

        self.attn = MultiHeadAttn(d_model, num_enc_heads)
        self.layer_norm1 = nn.LayerNorm(d_model)
        self.dropout1 = nn.Dropout(dropout)
        self.fc = PositionWiseFC(d_model, d_ff)
        self.layer_norm2 = nn.LayerNorm(d_model)
        self.dropout2 = nn.Dropout(dropout)

    def forward(self, x, pad_mask):
        # x+SubLayer(LayerNorm(x))
        layer_norm_x = self.layer_norm1(x)
        attn_score = self.attn(layer_norm_x, layer_norm_x, layer_norm_x, pad_mask) + x
        attn_score = self.dropout1(attn_score)

        # x+SubLayer(LayerNorm(x))
        attn_score = self.fc(self.layer_norm2(attn_score)) + attn_score
        attn_score = self.dropout2(attn_score)

        return attn_score # [B, S, D_model]

class MultiHeadAttn(nn.Module):
    def __init__(self, d_model: int, num_heads: int, attn_type: str='dot', dropout: float=0.1):
        super(MultiHeadAttn, self).__init__()

        self.attn_type = attn_type.lower()
        assert attn_type in ['dot', 'additive'], 'attn_type not supported: attn_type. Please select one of [dot, addictive]'

        assert d_model % num_heads == 0, 'the num_heads doesn\'t match'

        self.num_heads = num_heads
        self.d_k = int(d_model / num_heads)

        # Implements weights via `nn.Linear` instead `nn.Parameters()`
        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.softmax = nn.Softmax(dim=-1)

        if self.attn_type == 'additive':
            self.fc = nn.Linear(2*self.d_k, self.d_k)
            self.v = nn.Parameter(torch.rand(1, self.d_k))

        self.W_o = nn.Linear(d_model, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, k, v, q, mask):
        # k, v, q: [B, S, D_model]
        # mask: [B, S, S]
        attn_score = self.scaled_dot_product_attn(q, k, v, mask) # [B, S, D_model]
        attn_score = self.W_o(attn_score) # # [B, S, D_model]

        return attn_score

    def scaled_dot_product_attn(self, k, v, q, mask):
        # k, q, v: [B, S, D_model]
        # mask: [B, S, D_model]
        # FIXME: Attention에서도 사용할 수 있게 re-use 가능한가? ->
        batch_size, seq_len, _ = k.size()
        q = self.W_q(q).reshape(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) # [B, h, S, d_k]
        k = self.W_k(k).reshape(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) # [B, h, S, d_k]
        v = self.W_v(v).reshape(batch_size, seq_len, self.num_heads, self.d_k).transpose(1, 2) # [B, h, S, d_k]

        if self.attn_type == 'dot':
            qk = self.dot_attn(q, k)
        else:
            qk = self.additive_attn(q, k)

        # masking
        if mask != None:
            mask = mask.unsqueeze(1).repeat(1, self.num_heads, 1, 1) # [B, h, S, S]
            qk.masked_fill_(mask == True, -1e9)

        attn_score = self.softmax(qk) # [B, h, S, S]
        attn_score = attn_score.matmul(v) # [B, h, S, d_k]
        attn_score = attn_score.transpose(1, 2).reshape(batch_size, seq_len, self.d_k * self.num_heads) # [B, S, d_model]
        attn_score = self.dropout(attn_score)

        return attn_score

    def dot_attn(self, q, k):
        # $Attention(Q, K, V) = softmax(QK^T/\sqrt(d_k))V
        # q, k: # [B, h, S, d_k]
        qk = q.matmul(k.transpose(2, 3)) / torch.sqrt(torch.tensor(self.d_k, dtype=float)) # [B, h, S, S]
        return qk

    def additive_attn(self, q, k):
        # q, k: # [B, h, S, d_k]
        # Neural Machine Translation by Jointly Learning to Align and Translate
        # a(q, k) = w^T * tanh(W*[q:k])
        qk = torch.cat([q, k], dim=-1) # [B, h, S, 2*d_k]
        qk = self.fc(qk) # [B, h, S, d_k]
        qk = qk.permute(0, 1, 3, 2) # [B, d_k, S, d_k]
        v = self.v.reshape(1, 1, 1, -1) # [1, self.d_k] -> [1, 1, 1 self.d_k]
        v = v.repeat(q.size(0), -1, self.num_heads, 1).transpose(1, 2) # [B, h, S, self.d_k]
        qk = v.matul(qk) # [B, h, S, S]

        return qk

class PositionWiseFC(nn.Module):
    def __init__(self, d_model: int, d_ff: int, dropout: float=0.1):
        super(PositionWiseFC, self).__init__()

        self.fc1 = nn.Linear(d_model, d_ff)
        self.gelu = nn.GELU()
        self.fc2 = nn.Linear(d_ff, d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        # [B, S, D]
        x = self.fc1(x) # [B, S, D_ff]
        x = self.gelu(x) # [B, S, D_ff]
        x = self.fc2(x) # [B, S, D]
        x = self.dropout(x)

        return x

# reference: https://github.com/graykode/nlp-tutorial/blob/master/5-1.Transformer/Transformer(Greedy_decoder)_Torch.ipynb
def get_attn_pad_mask(seq_q, seq_k, pad_idx):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    pad_attn_mask = seq_k.data.eq(pad_idx).unsqueeze(1)  # [B, 1, S]
    return pad_attn_mask.expand(batch_size, len_q, len_k)  # [B, S_q, S_k]


import os
import logging
import json
import boto3
from botocore import exceptions

# Initiate logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logging.basicConfig(
    format="%(levelname)s %(threadName)s [%(filename)s:%(lineno)d] %(message)s",
    datefmt="%Y-%m-%d:%H:%M:%S",
    level=logging.INFO,
)
logger.info("Lambda initialization completed")

# Obtaining relevant Lambda environment variables
CROSS_ACCOUNT_ROLE_NAME = os.environ["CROSS_ACCOUNT_ROLE_NAME"]

def get_client(service, account_id):
    """
    Gets boto3 client after assuming the Config role in the target AWS account

    Parameters:
        service (str): the identifier of the AWS service to create a client for
        account_id (str): the AWS account ID to assume role in

    Returns:
        boto3 client: a boto3 client object
    """

    # Constructing role ARN from account ID and role name
    role_arn = "arn:aws:iam::" + account_id + ":role/" + CROSS_ACCOUNT_ROLE_NAME

    # Obtaining credentials by assuming role
    sts_client = boto3.client("sts")
    credentials = sts_client.assume_role(RoleArn=role_arn, RoleSessionName="configLambdaExecution")["Credentials"]

    return boto3.client(
        service,
        aws_access_key_id=credentials["AccessKeyId"],
        aws_secret_access_key=credentials["SecretAccessKey"],
        aws_session_token=credentials["SessionToken"],
    )

def get_ou_path(organizations_client, account_id):
    """
    Constructs the full OU path of the target account

    Parameters:
        organizations_client: boto3 client for AWS Organizations
        account_id (str): the target AWS account ID

    Returns:
        ou_path (str): the full OU path of the target account
    """

    ou_path = ""
    type = ""

    # Looping through all levels of the Organization until complete path os obtained
    while type != "ROOT":
        response = organizations_client.list_parents(ChildId=account_id)
        ou_id = response["Parents"][0]["Id"]
        type = response["Parents"][0]["Type"]

        if type != "ROOT":
            ou_name = organizations_client.describe_organizational_unit(OrganizationalUnitId=ou_id)[
                "OrganizationalUnit"
            ]["Name"]
            account_id = ou_id
            ou_path = "/" + ou_name + ou_path
            response = organizations_client.list_parents(ChildId=account_id)
            type = response["Parents"][0]["Type"]

    # Constructing full OU path using root OU name
    root_name = organizations_client.list_roots()["Roots"][0]["Name"]
    ou_path = "/" + root_name + ou_path

    logger.info("Starting Network Firewall remediation for account: " + account_id + " in OU: " + ou_path)

    return ou_path

def get_rule_group_mapping(ssm_client, spoke_ou_path, account_id):
    """
    Obtains the rule group mapping containing each rule group to apply to OU

    Parameters:
        ssm_client: boto3 client for AWS Systems Manager in the central security account
        spoke_ou_path (str): the full OU path of the target account
        account_id (str): the target AWS account ID

    Returns:
        mapping (dict): raw mapping of OU to rule group
    """

    # Reading SSM parameter containing rule groups to apply to the OU
    try:
        response = ssm_client.get_parameter(Name=spoke_ou_path)
    except ssm_client.exceptions.ParameterNotFound:
        mapping = "None"
        logger.info("No mapping found for OU path: " + spoke_ou_path + ". Evaluation skipped.")
    else:
        ou_baseline_mapping = json.loads(response["Parameter"]["Value"])

        # Iterating through SSM parameter and obtaining mapping for target account
        for account, rules in ou_baseline_mapping.items():
            if (account == "all_accounts" and account_id not in ou_baseline_mapping.keys()) or account == account_id:
                mapping = rules
                return mapping

    return

def get_rule_group_arns(raw_mapping):
    """
    Obtains the ARNs of each rule group to apply to each policy

    Parameters:
        raw_mapping: raw mapping of OU to rule group

    Returns:
        mapping (dict): mapping of required rule group types to rule group ARNs
    """

    processed_mapping = {}

    for rule_group_type, rule_group_list in raw_mapping.items():
        if rule_group_type not in processed_mapping:
            processed_mapping[rule_group_type.upper()] = []

        for rule_group_arn in rule_group_list:
            if rule_group_type.upper() == "STATEFUL" and type(rule_group_list) is list:
                processed_mapping[rule_group_type.upper()].append({"ResourceArn": rule_group_arn})
            else:
                processed_mapping[rule_group_type.upper()].append(
                    {
                        "ResourceArn": rule_group_arn,
                        "Priority": rule_group_list[rule_group_arn],
                    }
                )

    logger.info("Obtained the following firewall rule group mapping: " + json.dumps(processed_mapping))

    return processed_mapping

def remediate_rule_order(
    strict_order,
    policy,
    evaluation,
    result_token,
    spoke_anf_client,
    spoke_config_client,
):
    """
    Remediates non-compliant policies in the account if strict ordering is required.
    This function creates a new firewall policy with the correct rule order, and associates it with any firewalls in the account.

    Parameters:
        policy: description of AWS Network Firewall policy to be modified
        spoke_anf_client: boto3 client for AWS Network Firewall in the target account
        strict_order (str): whether or not to apply strict ordering to policy
    """

    policy_name = policy["Name"]
    spoke_policy = spoke_anf_client.describe_firewall_policy(FirewallPolicyName=policy_name)
    spoke_policy_details = spoke_policy["FirewallPolicy"]
    resource_arn = spoke_policy["FirewallPolicyResponse"]["FirewallPolicyArn"]
    rule_order = spoke_policy_details["StatefulEngineOptions"]["RuleOrder"]
    remediated_policy_name = None

    # Checking if policy has the correct stateful rule order, and modifying if not
    if strict_order == "true" and rule_order == "DEFAULT_ACTION_ORDER":
        # Getting details of existing policy for creation of new policy
        policy_arn = spoke_policy["FirewallPolicyResponse"]["FirewallPolicyArn"]
        description = (
            spoke_policy["FirewallPolicyResponse"]["Description"]
            if "Description" in spoke_policy["FirewallPolicyResponse"]
            else "Policy created by central firewall management"
        )
        tags = (
            spoke_policy["FirewallPolicyResponse"]["Tags"]
            if spoke_policy["FirewallPolicyResponse"]["Tags"] != []
            else [{"Key": "Name", "Value": policy_name}]
        )
        encryption_config = spoke_policy["FirewallPolicyResponse"]["EncryptionConfiguration"]
        remediated_policy_name = policy_name + "-baselined"

        # Setting rule order to strict, and removing all stateful rule groups from policy (these are not compatible with strict ordering)
        spoke_policy_details["StatefulEngineOptions"]["RuleOrder"] = "STRICT_ORDER"
        spoke_policy_details["StatefulEngineOptions"]["StreamExceptionPolicy"] = "DROP"
        spoke_policy_details["StatefulRuleGroupReferences"] = []

        # Creating new policy with strict rule ordering, as rule ordering on an existing policy cannot be modified
        try:
            response = spoke_anf_client.create_firewall_policy(
                FirewallPolicyName=remediated_policy_name,
                FirewallPolicy=spoke_policy_details,
                Description=description,
                Tags=tags,
                EncryptionConfiguration=encryption_config,
            )
        except exceptions.ClientError as e:
            logger.error("Could not create a new policy with strict rule ordering: " + remediated_policy_name)
            logger.error(e)
        else:
            remediated_policy_arn = response["FirewallPolicyResponse"]["FirewallPolicyArn"]
            logger.info("Created new policy with strict rule ordering: " + remediated_policy_name)

        # Updating firewalls in the account to associate the remediated firewall policy with
        firewall_list = spoke_anf_client.list_firewalls()["Firewalls"]
        for firewall in firewall_list:
            firewall_arn = firewall["FirewallArn"]
            spoke_firewall = spoke_anf_client.describe_firewall(FirewallArn=firewall_arn)
            update_token = spoke_firewall["UpdateToken"]

            if spoke_firewall["Firewall"]["FirewallPolicyArn"] == policy_arn:
                try:
                    response = spoke_anf_client.associate_firewall_policy(
                        UpdateToken=update_token,
                        FirewallArn=firewall_arn,
                        FirewallPolicyArn=remediated_policy_arn,
                    )
                except exceptions.ClientError as e:
                    logger.error(
                        "Could not associate remediated policy with strict rule ordering on following firewall: "
                        + firewall_arn
                    )
                    logger.error(e)
                else:
                    logger.info(
                        "Successfully associated remediated policy "
                        + remediated_policy_name
                        + " with firewall: "
                        + firewall_arn
                    )

        # Deleting non-compliant firewall policy
        try:
            response = spoke_anf_client.delete_firewall_policy(FirewallPolicyName=policy_name)
        except exceptions.ClientError as e:
            logger.error("Could not delete non-compliant firewall policy: " + policy_name)
            logger.error(e)
        else:
            logger.info("Deleted non-compliant firewall policy: " + policy_name)

            # If remediation is successful, reporting to AWS Config
            put_new_evaluation(
                spoke_config_client,
                evaluation,
                resource_arn,
                result_token,
            )

    return remediated_policy_name

def reprioritise_rule_groups(strict_order, rule_group_mapping, rule_group_list):
    """
    Modifies priority of existing rule groups to ensure baseline rule groups are evaluated first

    Parameters:
        rule_group_mapping (dict): mapping of baseline rule groups to apply to policy
        rule_group_list: list of current rule groups attached to policy
        strict_order (str): whether or not to apply strict ordering to policy

    Returns:
        rule_group_list (list): list of reprioritised rule groups
    """

    # Obtaining the maximum priority of baseline rule groups
    if strict_order == "true":
        priority_list = []
        for rule_group in rule_group_mapping:
            priority_list.append(int(rule_group["Priority"]))

            # Checking that priority of baseline rule groups is correct
            rule_group_list[:] = [x for x in rule_group_list if x["ResourceArn"] != rule_group["ResourceArn"]]

        max_baseline_priority = max(priority_list)
        min_priority = min(rule_group_list, key=lambda x: x["Priority"]) if len(rule_group_list) > 0 else 0

        # Increasing the priority of each existing rule group by the max baseline rule group, thus avoiding priority conflicts
        if min_priority != 0 and min_priority["Priority"] <= max_baseline_priority:
            delta = max_baseline_priority - min_priority["Priority"] + 1
            for i in range(len(rule_group_list)):
                rule_group_list[i]["Priority"] += delta

    return rule_group_list

def remediate_baseline_rule_groups(
    event,
    policy_list,
    strict_order,
    rule_group_mapping,
    spoke_config_client,
    spoke_anf_client,
):
    """
    Remediates non-compliant policies in the account if baseline rule groups are not applied

    Parameters:
        event: invoking event
        spoke_anf_client: boto3 client for AWS Network Firewall in the target account
        spoke_config_client: boto3 client for AWS Config in the target account
        rule_group_mapping (dict): mapping of baseline rule groups to apply to policy
        strict_order (str): whether or not to apply strict ordering to policy

    Returns:
        modified_resources (list): list of remediated resources
    """

    evaluations_list = event["Evaluations"]
    result_token = event["ResultToken"]
    modified_resources = []

    # Looping through non-compliant firewall policies to apply the correct baseline rule groups
    for evaluation in evaluations_list:
        resource_name = evaluation["ComplianceResourceId"]
        resource_type = evaluation["ComplianceResourceType"]
        compliance = evaluation["ComplianceType"]

        if compliance == "NON_COMPLIANT":
            for policy in policy_list:

                # Modifying rule order if required
                remediated_policy_name = remediate_rule_order(
                    strict_order,
                    policy,
                    evaluation,
                    result_token,
                    spoke_anf_client,
                    spoke_config_client,
                )
                if remediated_policy_name is None:
                    policy_name = policy["Name"]
                else:
                    policy_name = remediated_policy_name

                resource_name = "policy/" + policy_name
                spoke_policy = spoke_anf_client.describe_firewall_policy(FirewallPolicyName=policy_name)
                update_token = spoke_policy["UpdateToken"]
                spoke_policy_details = spoke_policy["FirewallPolicy"]

                if "STATEFUL" in rule_group_mapping:
                    # Reprioritising rule groups to enforce baseline rule groups first
                    stateful_rule_group_list = reprioritise_rule_groups(
                        strict_order,
                        rule_group_mapping["STATEFUL"],
                        spoke_policy_details["StatefulRuleGroupReferences"],
                    )

                    stateful_rule_group_list.extend(rule_group_mapping["STATEFUL"])

                    # Removing duplicate rule groups
                    unique_stateful_rules = [dict(t) for t in {tuple(d.items()) for d in stateful_rule_group_list}]

                    spoke_policy_details["StatefulRuleGroupReferences"] = unique_stateful_rules

                if "STATELESS" in rule_group_mapping:
                    # Reprioritising rule groups to enforce baseline rule groups first
                    stateless_rule_group_list = reprioritise_rule_groups(
                        strict_order,
                        rule_group_mapping["STATELESS"],
                        spoke_policy_details["StatelessRuleGroupReferences"],
                    )

                    stateless_rule_group_list.extend(rule_group_mapping["STATELESS"])

                    # Removing duplicate rule groups
                    unique_stateless_rules = [
                        dict(t)
                        for t in {tuple(d.items()) for d in spoke_policy_details["StatelessRuleGroupReferences"]}
                    ]

                    spoke_policy_details["StatelessRuleGroupReferences"] = unique_stateless_rules
                try:
                    response = spoke_anf_client.update_firewall_policy(
                        FirewallPolicyName=policy_name,
                        FirewallPolicy=spoke_policy_details,
                        UpdateToken=update_token,
                    )
                except exceptions.ClientError as e:
                    logger.error("Could not add rule groups to non-compliant policy: " + resource_name)
                    logger.error(e)
                else:
                    resource_arn = response["FirewallPolicyResponse"]["FirewallPolicyArn"]

                    # If remediation is successful, reporting to AWS Config
                    put_new_evaluation(
                        spoke_config_client,
                        evaluation,
                        resource_arn,
                        result_token,
                    )

                    # Creating list of modified firewall policies
                    modified_resources.append({"ResourceArn": resource_arn, "ResourceType": resource_type})

    return modified_resources

def remediate_all_rule_groups(
    event,
    policy_list,
    strict_order,
    rule_group_mapping,
    spoke_config_client,
    spoke_anf_client,
):
    """
    Remediates non-compliant policies in the account if excess rule groups are attached to a policy that are not centrally defined

    Parameters:
        event: invoking event
        spoke_anf_client: boto3 client for AWS Network Firewall in the target account
        spoke_config_client: boto3 client for AWS Config in the target account
        rule_group_mapping (dict): mapping of rule groups to apply to policy
        strict_order (str): whether or not to apply strict ordering to policy

    Returns:
        modified_resources (list): list of remediated resources
    """

    evaluations_list = event["Evaluations"]
    result_token = event["ResultToken"]
    modified_resources = []

    # Looping through non-compliant firewall policies to apply the correct baseline rule groups
    for evaluation in evaluations_list:
        resource_name = evaluation["ComplianceResourceId"]
        resource_type = evaluation["ComplianceResourceType"]
        compliance = evaluation["ComplianceType"]

        if compliance == "NON_COMPLIANT":
            for policy in policy_list:
                # Modifying rule order if required
                remediated_policy_name = remediate_rule_order(
                    strict_order,
                    policy,
                    evaluation,
                    result_token,
                    spoke_anf_client,
                    spoke_config_client,
                )

                if remediated_policy_name is None:
                    policy_name = policy["Name"]
                else:
                    policy_name = remediated_policy_name

                policy_name = policy["Name"]
                resource_name = "policy/" + policy_name
                spoke_policy = spoke_anf_client.describe_firewall_policy(FirewallPolicyName=policy_name)
                update_token = spoke_policy["UpdateToken"]
                spoke_policy_details = spoke_policy["FirewallPolicy"]

                if "STATEFUL" in rule_group_mapping:
                    spoke_policy_details["StatefulRuleGroupReferences"] = rule_group_mapping["STATEFUL"]

                if "STATELESS" in rule_group_mapping:
                    spoke_policy_details["StatelessRuleGroupReferences"] = rule_group_mapping["STATELESS"]

                try:
                    response = spoke_anf_client.update_firewall_policy(
                        FirewallPolicyName=policy_name,
                        FirewallPolicy=spoke_policy_details,
                        UpdateToken=update_token,
                    )
                except exceptions.ClientError as e:
                    logger.error("Could not enforce rule groups on non-compliant policy: " + resource_name)
                    logger.error(e)
                else:
                    resource_arn = response["FirewallPolicyResponse"]["FirewallPolicyArn"]

                    # If remediation is successful, reporting to AWS Config
                    put_new_evaluation(
                        spoke_config_client,
                        evaluation,
                        resource_arn,
                        result_token,
                    )

                    # Creating list of modified firewall policies
                    modified_resources.append({"ResourceArn": resource_arn, "ResourceType": resource_type})

                    logger.info("Successfully enforced centrally defined rule groups on policy: " + resource_name)

    return modified_resources

def put_new_evaluation(spoke_config_client, evaluation, resource_id, result_token):
    """
    Reports new evaluations to AWS Config after non-compliant resources have been remediated

    Parameters:
        evaluation (dict): original resource evaluations to modify
        spoke_anf_client: boto3 client for AWS Config in the target account
        resource_id: ID of resource that was remediated
        result_token (str): AWS Config results token

    Returns:
        eval_cc (dict): resource evalations for AWS Config
    """

    evaluation["ComplianceType"] = "COMPLIANT"
    evaluation["Annotation"] = resource_id

    response = spoke_config_client.put_evaluations(ResultToken=result_token, Evaluations=[evaluation])

    return response

def handler(event, context):
    account_id = event["accountId"]

    # Obtaining required boto3 clients
    spoke_anf_client = get_client("network-firewall", account_id)
    spoke_config_client = get_client("config", account_id)
    ssm_client = boto3.client("ssm")
    organizations_client = boto3.client("organizations")
    spoke_ou_path = get_ou_path(organizations_client, account_id)

    # Reading SSM parameter to obtain raw rule group mapping
    raw_rule_group_mapping = get_rule_group_mapping(ssm_client, spoke_ou_path, account_id)

    # Reads whether or not to enforce baselines only, then removing from rule group mapping
    baseline_plus = raw_rule_group_mapping["baseline_plus"]
    del raw_rule_group_mapping["baseline_plus"]

    # Reads whether or not to enforce strict order, then removing from rule group mapping
    strict_order = raw_rule_group_mapping["strict_order"]
    del raw_rule_group_mapping["strict_order"]

    # Obtaining ARNs of firewall rule groups for mapping
    processed_rule_group_mapping = get_rule_group_arns(raw_rule_group_mapping)

    # Get list of all firewall policies in target account
    policy_list = spoke_anf_client.list_firewall_policies()["FirewallPolicies"]

    # Remediating all firewall policies
    if baseline_plus == "true":
        modified_resources = remediate_baseline_rule_groups(
            event,
            policy_list,
            strict_order,
            processed_rule_group_mapping,
            spoke_config_client,
            spoke_anf_client,
        )
    else:
        modified_resources = remediate_all_rule_groups(
            event,
            policy_list,
            strict_order,
            processed_rule_group_mapping,
            spoke_config_client,
            spoke_anf_client,
        )

    logger.info(
        "The following resources were modified in account " + account_id + ": " + json.dumps(modified_resources)
    )

    return


import pygame as pg
import random as rnjuzus
import keyboard as key
import time
from tkinter import *
from pygame.font import *

###Function###
Play_YL3=False
Play_YL1=False
Open_info=False
Open_settings=False
softwareDisplayed=False
Stats=(0,0)
state_info_button=0
state_settings_button=0
state_yl1_button=0
state_yl3_button=0
Window=2
money=500
storeAppLit=False
gamesAppLit=False
channelName='mightypaw'
stat_timer=1
s_icon_open=False
closepos=(0,0)
Settings_open=False
pg.init()
disp=pg.display.set_mode((1900,1030))
pg.display.set_caption('YouLose 3')
clock=pg.time.Clock()
white=(200,200,200)

def MOUSE():
    global mouse_pos,click
    mouse_pos=pg.mouse.get_pos()
    click=pg.mouse.get_pressed()

def NECESSITIES():
    global mouse_pos,click,open,Stats,money
    mouse_pos=pg.mouse.get_pos()
    click=pg.mouse.get_pressed()
    if key.is_pressed('0'):
        money=money+1
        UPDATE_ALL()
    if key.is_pressed('9'):
        money=money+0.01
        UPDATE_ALL()
    if key.is_pressed('-'):
        money=money-0.01
        UPDATE_ALL()
    for event in pg.event.get():
        print(event)
        if event.type==pg.QUIT:
            open=False
            quit()
    pg.display.update()
    clock.tick(60)

def TEXT_OBJECTS(text,font):
    Text_surface=font.render(text,False,white)
    return Text_surface,Text_surface.get_rect()

def MSG_DISPLAY(text,subject,x,y):
    global med_text,Subs
    Med_text=pg.font.Font('C:\Windows\Fonts\COUR.ttf',50)
    TextSurf,TextRect=TEXT_OBJECTS(text,Med_text)
    TextRect.center=((x-120)+(len(str(subject))*15),y)
    disp.blit(TextSurf,TextRect)

def MONEY_ROUND():
    global money
    money=money*100
    money=int(money)
    money=money/100

def MOUSE_HOVER_BUTTON():
    global Window,state_info_button,state_settings_button,state_yl1_button,state_yl3_button,Play_YL3,Play_YL1,\
    Open_settings,Open_info
    if 550<mouse_pos[0]<850 and 400<mouse_pos[1]<460:
        if state_info_button == 0:
            disp.blit(info_2, (550, 400))
            state_info_button = 1
    elif state_info_button == 1 and not 550<mouse_pos[0]<850 or not 400<mouse_pos[1]<460:
            disp.blit(info_1, (550, 400))
            state_info_button = 0

    if 1050<mouse_pos[0]<1350 and 400<mouse_pos[1]<460:
        if state_settings_button == 0:
            disp.blit(settings_2, (1050, 400))
            state_settings_button = 1
    elif state_settings_button == 1 and not 1050 < mouse_pos[0] < 1350 or not 400 < mouse_pos[1] < 460:
            disp.blit(settings_1, (1050, 400))
            state_settings_button = 0

    if 550<mouse_pos[0]<850 and 560<mouse_pos[1]<620:
        if state_yl1_button == 0:
            disp.blit(YL1_2, (550, 560))
            state_yl1_button = 1
    elif state_yl1_button == 1 and not 550 < mouse_pos[0] < 850 or not 560 < mouse_pos[1] < 620:
            disp.blit(YL1_1, (550, 560))
            state_yl1_button = 0

    if 1050<mouse_pos[0]<1350 and 560<mouse_pos[1]<620:
        if state_yl3_button==0:
            disp.blit(YL3_2,(1050,560))
            state_yl3_button=1
    elif state_yl3_button==1 and not 1050<mouse_pos[0]<1350 or not 560<mouse_pos[1]<620:
        disp.blit(YL3_1,(1050,560))
        state_yl3_button=0

    if click[0]==1:
        if 1050<mouse_pos[0]<1350 and 560<mouse_pos[1]<620:   #yl3
            Play_YL3=True
            REL_ALL_YL3()
            Window=2
        elif 550<mouse_pos[0]<850 and 560<mouse_pos[1]<620:   #yl1
            Play_YL1=True
            YE_OLDE()
        elif 1050<mouse_pos[0]<1350 and 400<mouse_pos[1]<460: #settings
            Open_settings=True
        elif 550<mouse_pos[0]<850 and 400<mouse_pos[1]<460:   #info
            Open_info=True
        else:
            pass

def REL_ALL_YL3():
    BACK(0,0)
    SETTINGS_ICON_REL(947,5)
    WI2(635,20)
    WI1(325,20)
    WI0(15,20)
    WINDOW_BACKGROUND()
    UPDATE_ALL()
    if Window==0:
        URL_BOTS(1510, 20)
        WINC(330,75)
    elif Window==2:
        URL_RECORD(1510, 20)
        WINC(20,75)
        APPLICATIONS()
    else:
        URL_UPLOAD(1510,20)
        CH_PROFILE()
        WINC(640,75)

def CLICK_EVENT_CHANNEL():
    if 280<mouse_pos[0]<880 and 90<mouse_pos[1]<170 and click[0]==1:
        CH_RENAME()

def CLICK_EVENT_SOFTWARE():
    global gamesAppLit,storeAppLit,gamesApp2,storeApp2
    if 30<mouse_pos[0]<290 and 100<mouse_pos[1]<360 and gamesAppLit==False:
        disp.blit(gamesApp2,(30,100))
        gamesAppLit=True
    elif 300<mouse_pos[0]<560 and 100<mouse_pos[1]<360 and storeAppLit==False:
        disp.blit(storeApp2,(300,100))
        storeAppLit=True
    elif not 30<mouse_pos[0]<290 or not 100<mouse_pos[1]<360 and not gamesAppLit==False:
        disp.blit(gamesApp,(30,100))
        gamesAppLit=False
    elif not 300<mouse_pos[0]<560 or not 100<mouse_pos[1]<360 and not storeAppLit==False:
        disp.blit(storeApp,(300,100))
        storeAppLit=False
    else:
        pass

stat_cover=pg.image.load('money_box.png')
def STAT_COVER(x,y):
    disp.blit(stat_cover,(x,y))

def UPDATE_STATS_TIMED():
    global stat_timer,money
    stat_timer=stat_timer-1
    if stat_timer<0:
        stat_timer=5
        STAT_COVER(1510,90)
        MONEY_ROUND()
        MSG_DISPLAY('£'+str(money),money,1660,120)
        STAT_COVER(1510, 160)
        MSG_DISPLAY(u'\u25ba'+str(Stats[0]),Stats[0], 1660, 190)
        STAT_COVER(1510, 230)
        MSG_DISPLAY('V' + str(Stats[1]), Stats[1], 1660, 260)
def UPDATE_ALL():
    global stat_timer,money
    stat_timer=30
    STAT_COVER(1510,90)
    MONEY_ROUND()
    MSG_DISPLAY('£'+str(money),money, 1660, 120)
    STAT_COVER(1510,160)
    MSG_DISPLAY(u'\u25ba'+str(Stats[0]),Stats[0], 1660, 190)
    STAT_COVER(1510,230)
    MSG_DISPLAY('V' + str(Stats[1]), Stats[1], 1660, 260)

def YE_OLDE():
    Relevant=True
    root = Tk()
    DaysWait = 1
    stream = False
    rec_done = False
    spaces = ''
    spaceamount = 0
    QUALITY = 0
    Name = 'i was too lazy to name my channel :/'

    def snooze(event):
        global Creation
        # print('omg this is working')
        Creation = False
        root.destroy()

    def makevid(event):
        global Creation
        # print('omg this is working 2')
        Creation = True
        root.destroy()

    def proceed(event):
        root.destroy()

    def showname(event):
        global Name, FullName
        print('name:', (namechannel_entry.get()))
        Name = namechannel_entry.get()
        FullName = 'Logged in as: ' + Name

    def waiting(event):
        global DaysWait
        print('wait:', (howlong_entry.get()))
        DaysWait = (howlong_entry.get())
        DaysWait = int(DaysWait)
        print(DaysWait)

    def streaming(event):
        global stream
        stream = True
        root.destroy()

    def recording(event):
        global stream
        stream = False
        root.destroy()

    def stop(event):
        global spaceamount, rec_done
        if spaceamount == 5:
            wow()
        elif spaceamount == 5:
            good()
        elif spaceamount == 6 or spaceamount > 11 and spaceamount < 17:
            med()
        else:
            bad()
        rec_done = True
        root.destroy()

    def delete():
        root.destroy()

    def bad():
        global QUALITY
        QUALITY = 2

    def med():
        global QUALITY
        QUALITY = 3

    def good():
        global QUALITY
        QUALITY = 4

    def wow():
        global QUALITY
        QUALITY = 6

    #######################################################
    welcome_label = Label(root, text='Welcome 2 TwitchSim')
    Name = StringVar()
    namechannel_label = Label(root, text='Name ur channel')
    namechannel_entry = Entry(root, textvariable=Name)
    continue_button = Button(root, text='[Continue]')
    showname_button = Button(root, text='Save name')

    showname_button.bind('<Button-1>', showname)
    continue_button.bind('<Button-1>', proceed)

    showname_button.grid(row=2, sticky=W)
    welcome_label.grid(row=0, columnspan=2, sticky=W)
    namechannel_label.grid(row=1, sticky=E)
    namechannel_entry.grid(row=1, column=1)
    continue_button.grid(row=2, column=1, sticky=E)
    root.mainloop()
    print(Name)
    while Relevant == True:
        root = Tk()
        loop_button = Button(root, text='   [b lazy]    ')
        loop_button.bind('<Button-1>', snooze)

        create_button = Button(root, text='[Create content]')
        create_button.bind('<Button-1>', makevid)

        loop_button.pack(side=LEFT)
        create_button.pack(side=RIGHT)
        root.mainloop()
        if Creation == False:
            root = Tk()
            wait = IntVar()
            howlong_label = Label(root, text='wait for how long?')
            howlong_entry = Entry(root, textvariable=wait)
            continue_button2 = Button(text='[Continue]')

            enter_button = Button(root, text='Press this before continue')
            continue_button2.bind('<Button-1>', proceed)
            enter_button.bind('<Button-1>', waiting)

            enter_button.grid(row=1, sticky=W)
            continue_button2.grid(row=1, column=1, sticky=E)
            howlong_label.grid(row=0)
            howlong_entry.grid(row=0, column=1)
            root.mainloop()
            # wait=int(wait)
            while DaysWait > 0:
                time.sleep(0.5)
                print('Iterations remaining:', DaysWait)
                DaysWait = DaysWait - 1
            time.sleep(0.5)
            Creation = True
        else:
            root = Tk()
            decision_label = Label(root, text='streaming TBA')
            stream_button = Button(root, text='[Begin stream]')
            record_button = Button(root, text='[Start recording]')

            stream_button.bind('<Button-1>', streaming)
            record_button.bind('<Button-1>', recording)

            decision_label.grid(row=0, columnspan=2, sticky=W)
            stream_button.grid(row=1, sticky=W)
            record_button.grid(row=1, column=1, sticky=E)
            root.mainloop()
            if stream == True:
                print('stream feature not added yet')
                stream = False
            else:
                rec_done = False
                #root=Tk()
                text = Text(root)
                text.configure(font=("Courier new", 16, "bold"))
                while rec_done == False:
                    root = Tk()
                    if spaceamount < 20:
                        spaces = spaces + '_'
                        spaceamount = spaceamount + 1
                    else:
                        spaces = ''
                        spaceamount = 0
                    arrow = spaces + '^'
                    nameL = Label(root, text=FullName)
                    recL = Label(root, text='now recording.stop arrow under heavier shade for higher quality.')
                    barL = Label(root, text=' ░░░░█▒░░░░░▒▒▒▒▒▓░▓░')
                    arrowL = Label(root, text=arrow)
                    stopB = Button(root, text='[stop]')

                    stopB.bind('<Button-1>', stop)
                    nameL.grid(row=2, column=1, sticky=E)
                    recL.grid(row=0, columnspan=2)
                    barL.grid(row=1, sticky=W)
                    arrowL.grid(row=2, sticky=W)
                    stopB.grid(row=1, column=1, sticky=W)
                    root.after(500,lambda:root.destroy())
                    root.mainloop()

def CLICK_EVENT_WINDOW():
    WI0(15, 20)
    WI1(325, 20)
    WI2(635, 20)

def CH_RENAME():
    global channelName,entry666,root5
    def getName(event):
        global channelName, entry666, root5
        channelName = entry666.get()
        root5.destroy()
    print('tk not work dud')
    root5=Tk()
    label=Label(root5,text='Rename your channel:')
    entry666=Entry(root5,textvariable=channelName)
    button=Button(root5,text='OK')
    button.bind('<Button-1>',getName)
    label.pack(side=LEFT)
    entry666.pack()
    button.pack(side=RIGHT)
    root5.mainloop()
    MSG_DISPLAY(channelName,channelName,610,125)
    REL_CHANNEL()

def SETTINGS_BUTTONS():
    global Play_YL1,Play_YL3,Open_settings,Open_info,Settings_open
    if 1550<mouse_pos[0]<1850 and 960<mouse_pos[1]<1040 and click[0]==1 and Open_settings==True:
        Play_YL3=False
        Open_settings=False
        Settings_open=False
        REL_ALL_START()

def REL_CHANNEL():
    CH_PROFILE()
    MSG_DISPLAY(channelName, channelName, 610, 125)

#def REL_RECORD():

#def REL_BOTS():

def BUTTON_WINDOW():
    global Window, Settings_open,s_icon_open,Open_settings
    if mouse_pos[1]<80 and Play_YL3==True:
        if 15<mouse_pos[0]<315 and 20<mouse_pos[1]<80 and click[0]==1:
            CLICK_EVENT_WINDOW()
            URL_RECORD(1510,20)
            WINC(20,75)
            WINDOW_BACKGROUND()
            APPLICATIONS()
            Window=2
            #software

        elif 325<mouse_pos[0]<625 and 20<mouse_pos[1]<80 and click[0]==1:
            CLICK_EVENT_WINDOW()
            URL_UPLOAD(1510,20)
            WINC(330,75)
            WINDOW_BACKGROUND()
            REL_CHANNEL()
            Window=1
            #channel

        elif 635<mouse_pos[0]<945 and 20<mouse_pos[1]<80 and click[0]==1:
            CLICK_EVENT_WINDOW()
            URL_BOTS(1510,20)
            WINC(640,75)
            WINDOW_BACKGROUND()
            #REL_BOTS()
            Window=0
            #bots

        elif 947<mouse_pos[0]<1017 and 5<mouse_pos[1]<75:
            if s_icon_open == False:
                SETTINGS_ICON_2(947, 5)
                s_icon_open=True
            elif click[0]==1 and Settings_open==False:
                Settings_open=True
                Open_settings=True
                SETTINGS_P(0,0)
                X_BUTTON(1840, 10)
                if 947<mouse_pos[0]<1017 and 5<mouse_pos[1]<75:
                    if not s_icon_open==True:
                        SETTINGS_ICON_2(947,5)
                        s_icon_open=True
                    else:
                        pass
                else:
                    pass
        elif s_icon_open==True and not 947<mouse_pos[0]<1017 or not 5<mouse_pos[1]<75:
            SETTINGS_ICON_REL(947,5)
            s_icon_open=False
        elif 1840<mouse_pos[0]<1900 and 10<mouse_pos[1]<70\
                and click[0]==1 and Settings_open==True:
            REL_ALL_YL3()
            s_icon_open=False
            Settings_open=False
            Open_settings=False

###Graphics###
bg=pg.image.load('YL3back.png')
def BACK(bX,bY):
    disp.blit(bg,(bX,bY))

xout=pg.image.load('x-out.png')
def X_BUTTON(x,y):
    disp.blit(xout,(x,y))

exit_yl3=pg.image.load('Exit_yl3.png')
settings_pg=pg.image.load('yl-settings-0.png')
def SETTINGS_P(x,y):
    disp.blit(settings_pg,(x,y))
    disp.blit(exit_yl3, (1550, 960))

W0=pg.image.load('softwareWindow.png')
def WI0(w0x,w0y):
    disp.blit(W0,(w0x,w0y))
W1=pg.image.load('channel window.png')
def WI1(w1x,w1y):
    disp.blit(W1,(w1x,w1y))
W2=pg.image.load('bots window.png')
def WI2(w2x,w2y):
    disp.blit(W2,(w2x,w2y))

Settings_icon_opened=pg.image.load('YL-settings-selected.png')
Settings_icon=pg.image.load('YL-settings-default.png')
def SETTINGS_ICON_REL(x,y):
    disp.blit(Settings_icon,(x,y))
def SETTINGS_ICON_2(x,y):
    disp.blit(Settings_icon_opened,(x,y))

wc=pg.image.load('windowcover.png')
def WINC(wcx,wcy):
    disp.blit(wc,(wcx,wcy))

wb=pg.image.load('windowBackgroundv3.png')
def WINDOW_BACKGROUND():
    disp.blit(wb,(0,80))

nb=pg.image.load('ch_name_box.png')
img123=pg.image.load('123.png')
donate_img=pg.image.load('donate.png')
merch_img=pg.image.load('merch.png')
def CH_PROFILE():
    disp.blit(nb,(280,90))
    disp.blit(img123,(10,90))
    disp.blit(donate_img,(280,180))
    disp.blit(merch_img,(590,180))

gamesApp=pg.image.load('gamesApplication.png')
gamesApp2=pg.image.load('gamesApplication2.png')
storeApp=pg.image.load('storeApplication.png')
storeApp2=pg.image.load('storeApplication2.png')
def APPLICATIONS():
    disp.blit(gamesApp,(30,100))
    disp.blit(storeApp,(300,100))

def REL_ALL_START():
    global info_1,info_2,settings_1,settings_2,YL1_1,YL1_2,YL3_1,YL3_2,start_bg
    start_bg=pg.image.load('bad background.png')
    logo=pg.image.load('YouLose logo.png')
    info_1=pg.image.load('INFO_button.png')
    info_2=pg.image.load('INFO_button_1.png')
    settings_1=pg.image.load('SETTINGS_button.png')
    settings_2=pg.image.load('SETTINGS_button_1.png')
    YL1_1=pg.image.load('YL1_button.png')
    YL1_2=pg.image.load('YL1_button_1.png')
    YL3_1=pg.image.load('YL3_button.png')
    YL3_2=pg.image.load('YL3_button_1.png')

    disp.blit(start_bg,(0,0))
    disp.blit(logo,(862,100))
    disp.blit(info_1,(550,400))
    disp.blit(settings_1,(1050,400))
    disp.blit(YL1_1,(550,560))
    disp.blit(YL3_1,(1050,560))

url_record=pg.image.load('url-record.png')
def URL_RECORD(x,y):
    disp.blit(url_record,(x,y))
url_upload=pg.image.load('url-upload.png')
def URL_UPLOAD(x,y):
    disp.blit(url_upload,(x,y))
url_bots=pg.image.load('url-bots.png')
def URL_BOTS(x,y):
    disp.blit(url_bots,(x,y))

winCx=25
winCy=75

open=True
REL_ALL_START()
while open:
    NECESSITIES()
    MOUSE_HOVER_BUTTON()
    while Play_YL3==True:
        BUTTON_WINDOW()
        NECESSITIES()
        UPDATE_STATS_TIMED()
        if Settings_open:
            SETTINGS_BUTTONS()
        elif Window==0:
            pass
        elif Window==1:
            CLICK_EVENT_CHANNEL()
        elif Window==2:
            CLICK_EVENT_SOFTWARE()
    if Play_YL1==True:
        YE_OLDE()


# Cayden Ehrlich
# Intro CS, Winter 2017
# Final Project: Carleton College - The Quest for Schiller

import random

def intro():
	# I got the ASCII Art for the title using a website (http://patorjk.com/software/taag/#p=display&f=Alpha&t=Type%20Something%20)
	print('''
		   ___           _      _                  ___      _ _
		  / __\__ _ _ __| | ___| |_ ___  _ __     / __\___ | | | ___  __ _  ___ _
		 / /  / _` | '__| |/ _ \ __/ _ \| '_ \   / /  / _ \| | |/ _ \/ _` |/ _ (_)
		/ /__| (_| | |  | |  __/ || (_) | | | | / /__| (_) | | |  __/ (_| |  __/_
		\____/\__,_|_|  |_|\___|\__\___/|_| |_| \____/\___/|_|_|\___|\__, |\___(_)
		                                                             |___/

		  ________            ____                  __     ____              _____      __    _ ____
		 /_  __/ /_  ___     / __ \__  _____  _____/ /_   / __/___  _____   / ___/_____/ /_  (_) / /__  _____
		  / / / __ \/ _ \   / / / / / / / _ \/ ___/ __/  / /_/ __ \/ ___/   \__ \/ ___/ __ \/ / / / _ \/ ___/
		 / / / / / /  __/  / /_/ / /_/ /  __(__  ) /_   / __/ /_/ / /      ___/ / /__/ / / / / / /  __/ /
		/_/ /_/ /_/\___/   \___\_\__,_/\___/____/\__/  /_/  \____/_/      /____/\___/_/ /_/_/_/_/\___/_/

WELCOME TO CARLETON COLLEGE: THE QUEST FOR SCHILLER
For years now, the bust of Schiller, part of important Carleton College tradition, has been missing.
It's been so long since it's been seen that some students don't even think that the bust exists, and that it's just a \
joke made up by upperclassmen.
You know that the bust of Schiller is real, though. You may be the only one who still believes that it exists.
You've heard rumors that the bust has been stolen by monsters and is in a secret dungeon hidden under the first \
floor of the library. You, being a brave \
Carleton student, are determined to find the bust of Schiller and prove that it still exists, and so you have found the secret dungeon and are ready to find Schiller, \
whatever it takes.
Good luck.
Type help for a list of commands.
''')

# Item params = itemIDList, itemsDict, description, moveable, locDescript
class Item:
	'''This will be a base class for all items in the game - weapons, armor, gold, etc.'''
	def __init__(self, itemIDList, itemsDict, description, moveable, locDescript):
		'''
		itemIDList will be a list of the strings with which the player may refer to the item.
		itemsDict will be a dictionary in which all potential string references to an item will be stored as keys with the value being the actual item.
		description will be a string describing the item (ex: 'A shiny steel sword.').
		moveable will be a Boolean letting us know if the item can be moved. This will allow the player to still examine objects that can't be picked up.
		locDescript will be a description of where the item is (ex: 'There is a sword on the table.'). This will be changed if the item is dropped.
		'''
		self.descript = description
		self.moveable = moveable
		self.locDescript = locDescript
		self.itemIDList = itemIDList
		for itemID in self.itemIDList:
			itemsDict[itemID] = self
		self.itemType = 'item'
		if itemIDList[0][0] in ['a', 'e', 'i', 'o', 'u']:
			self.article = 'an'
		else:
			self.article = 'a'

	def getLocDescript(self):
		'''Describes where the item is.'''
		return self.locDescript

	def examine(self):
		'''Called by the Player class when the player wants a description of an item.'''
		return self.descript

# Weapon params = itemIDList, itemsDict, description, locDescript, damage
class Weapon(Item):
	def __init__(self, itemIDList, itemsDict, description, locDescript, damage):
		'''Weapons will increase the player's maximum possible damage.'''
		self.moveable = True
		Item.__init__(self, itemIDList, itemsDict, description, self.moveable, locDescript)
		self.damage = damage
		self.equipped = False
		self.itemType = 'weapon'

	def examine(self):
		'''Weapons have their own examine function so that it will also show the damage and whether or not it is equipped.'''
		weaponDescript = self.descript
		weaponDescript += ' ({} damage)'.format(self.damage)
		if self.equipped:
			weaponDescript += '   (Equipped)'
		return weaponDescript

# Armor params = itemIDList, itemsDict, description, locDescript, defense, armorSlot
class Armor(Item):
	def __init__(self, itemIDList, itemsDict, description, locDescript, defense, armorSlot):
		'''Armor will increase the player's defense against enemies. The player will be able to have one helmet, one chest piece, one pair of leggings, and one pair of boots.'''
		self.moveable = True
		Item.__init__(self, itemIDList, itemsDict, description, self.moveable, locDescript)
		self.defense = defense
		self.isEquipped = False
		self.armorSlot = armorSlot
		self.itemType = 'armor'

	def examine(self):
		'''Similar to the weapon examine function, but defense instead of damage. Also says which slot the armor can fill.'''
		armorDescript = self.descript
		armorDescript += ' ({} defense. {})'.format(self.defense, self.armorSlot)
		if self.equipped:
			armorDescript += '   (Equipped)'
		return armorDescript

# Schillers params = itemsDict, locDescript, value
class Schillers(Item):
	def __init__(self, itemsDict, locDescript, value):
		moveable = True
		self.value = value
		descript = "A coin with Schiller's bust on one side. The other side has the number {} on it.".format(self.value)
		Item.__init__(self, ['schillers', 'coins', 'gold', '{} schillers'.format(self.value)], itemsDict, descript, moveable, locDescript)
		self.itemType = 'schillers'
		self.article = str(self.value)

# Enemy params = enemyID, enemiesList, HP, damage, defense, itemsToDrop
class Enemy:
	# Note: itemsToDrop will be a list of actual items, not just strings; must initiate items BEFORE enemies
	def __init__(self, enemyID, enemiesList, HP, maxDamage, defense, itemsToDrop):
		self.enemyID = enemyID
		self.HP = HP
		self.maxDamage = maxDamage
		self.defense = defense
		self.itemsToDrop = itemsToDrop
		self.enemiesList = enemiesList
		self.enemiesList.append(self)

	def isAlive(self):
		if self.HP > 0:
			return True
		else:
			return False

	def attackPlayer(self, Player):
		damage = int(random.gauss(self.maxDamage/2.0, 1.5))
		damMinusDef = damage - Player.defense
		if damMinusDef < 0:
			damMinusDef = 0
		Player.HP -= damMinusDef
		if damMinusDef > 0:
			msg = '{} attacks you for {} damage! You have {} HP left.'.format(self.enemyID.capitalize(), damMinusDef, Player.HP)
			if damage > self.maxDamage:
				msg = 'CRITICAL HIT! ' + msg
		else:
			msg = '{} missed! You have {} HP left.'.format(self.enemyID.capitalize(), Player.HP)
		return msg

	def die(self, room):
		room.hasEnemy = False
		self.enemiesList.remove(self)
		dropsNameList = []
		for item in self.itemsToDrop:
			room.dropIntoRoom(item)
			dropsNameList.append(item.article + ' ' + item.itemIDList[0])
		if len(dropsNameList) > 1:
			dropsNameStr = ', '.join(dropsNameList[:-1])
			dropsNameStr += ' and {}'.format(dropsNameList[-1])
		else:
			dropsNameStr = ''.join(dropsNameList)
		return '{} died, dropping {}.'.format(self.enemyID.capitalize(), dropsNameStr)

class BasePuzzle:
	def __init__(self, puzzleDescription, victoryMsg, hintsList, puzzlesList):
		self.hintsList = hintsList
		self.victoryMsg = victoryMsg
		self.hintsCounter = 1
		self.solved = False
		self.puzzleDescription = puzzleDescription
		self.puzzlesList = puzzlesList
		self.puzzlesList.append(self)

	def solve(self):
		self.solved = True
		self.puzzlesList.remove(self)
		self.puzzleDescription = 'This puzzle has been solved.'
		return self.victoryMsg

	def getHints(self):
		'''This will give the user hints from the list of hints if they need it. They will get one more hint (if there are more) each time they ask.'''
		givenHintsList = []
		for hintIndex in range(self.hintsCounter):
			givenHintsList.append(self.hintsList[hintIndex])
		if self.hintsCounter < len(self.hintsList):
			self.hintsCounter += 1
		hintsStr = '\n'.join(givenHintsList)
		return hintsStr

# SingleItemPuzzle params = puzzleDescription, victoryMsg, hintsList, puzzlesList, itemToUse
class SingleItemPuzzle(BasePuzzle):
	def __init__(self, puzzleDescription, victoryMsg, hintsList, puzzlesList, itemToUse):
		BasePuzzle.__init__(self, puzzleDescription, victoryMsg, hintsList, puzzlesList)
		self.itemToUse = itemToUse
		self.puzType = 'single'

	def solveAttempt(self, itemObj, Player):
		if itemObj == self.itemToUse:
			Player.inventory.remove(itemObj)
			return BasePuzzle.solve(self)
		else:
			return 'Nothing happens.'

# OrderPuzzle params = puzzleDescription, victoryMsg, hintsList, puzzlesList, objectOrderList, newLocDescript
class OrderPuzzle(BasePuzzle):
	def __init__(self, puzzleDescription, victoryMsg, hintsList, puzzlesList, objectOrderList, newLocDescript):
		BasePuzzle.__init__(self, puzzleDescription, victoryMsg, hintsList, puzzlesList)
		self.objectOrderList = objectOrderList
		self.playerObjectUse = []
		self.puzType = 'order'
		self.newLocDescript = newLocDescript

	def solveAttempt(self, itemObj, Player):
		self.playerObjectUse.append(itemObj)
		if self.newLocDescript != '':
			itemObj.locDescript = 'The {} {}'.format(itemObj.itemIDList[0], self.newLocDescript)
		latestIndex = len(self.playerObjectUse) - 1
		if self.playerObjectUse[latestIndex] == self.objectOrderList[latestIndex]:
			if len(self.playerObjectUse) == len(self.objectOrderList):
				return BasePuzzle.solve(self)
			else:
				return "You hear something happening...maybe gears turning? It seems like you made the right choice."
		else:
			self.playerObjectUse = []
			return "Nothing happens...you must have made a wrong choice. Try again."

# BaseRoom params = roomID, roomsDict, directionsDict, description, itemsList
class BaseRoom:
	def __init__(self, roomID, roomsDict, directionsDict, description, itemsList):
		self.directionsDict = directionsDict
		self.descript = description
		self.itemsList = itemsList
		self.roomID = roomID
		roomsDict[self.roomID] = self
		self.roomsDict = roomsDict
		self.hasEnemy = False
		self.hasPuzzle = False
		self.isShop = False

	def isLegalMove(self, direct):
		shortDirect = direct[0]
		return shortDirect in self.directionsDict

	def getMoveDestination(self, direct):
		newRoomStr = self.directionsDict[direct]
		newRoom = self.roomsDict[newRoomStr]
		return newRoom

	def canTake(self, item):
		if item in self.itemsList:
			return True
		else:
			return False

	def takeFromRoom(self, item):
		self.itemsList.remove(item)

	def dropIntoRoom(self, item):
		item.locDescript = 'There is/are {} {} on the floor.'.format(item.article, item.itemIDList[0])
		self.itemsList.append(item)

	def describe(self):
		totalDescriptionL = []
		totalDescriptionL.append(self.descript)
		for item in self.itemsList:
			if item.getLocDescript() != '':
				totalDescriptionL.append('\n' + item.getLocDescript())
		descriptionStr = ' '.join(totalDescriptionL)
		totalDescription = (self.roomID.upper() + '\n') + descriptionStr
		return totalDescription

# EnemyRoom params = roomID, roomsDict, directionsDict, description, itemsList, enemy
class EnemyRoom(BaseRoom):
	def __init__(self, roomID, roomsDict, directionsDict, description, itemsList, enemyObj):
		BaseRoom.__init__(self, roomID, roomsDict, directionsDict, description, itemsList)
		self.hasEnemy = True
		self.enemy = enemyObj

	def describe(self):
		description = BaseRoom.describe(self)
		if self.hasEnemy:
			description += '\nThere is a(n) {} in the room!'.format(self.enemy.enemyID)
		return description

# PuzzleRoom params = roomID, roomsDict, directionsDict, description, itemsList, puzzle, lockedDirectionDict, puzzPrizes
class PuzzleRoom(BaseRoom):
	def __init__(self, roomID, roomsDict, directionsDict, description, itemsList, puzzle, lockedDirectionDict, puzzPrizes):
		BaseRoom.__init__(self, roomID, roomsDict, directionsDict, description, itemsList)
		self.hasPuzzle = True
		self.puzzle = puzzle
		self.lockedDirectionDict = lockedDirectionDict
		self.puzzPrizes = puzzPrizes

	def describe(self):
		noPuzDescript = BaseRoom.describe(self)
		PuzDescript = noPuzDescript + '\n{}'.format(self.puzzle.puzzleDescription)
		if self.lockedDirectionDict != {}:
			directionTranslator = {'n': 'North', 's': 'South', 'e': 'East', 'w': 'West'}
			directionKey = ''.join(self.lockedDirectionDict.keys())
			self.fullDirection = directionTranslator[directionKey]
			PuzDescript += '\nThere is a locked door to the {}.'.format(self.fullDirection)
		return PuzDescript

	def puzzleSolve(self, itemObj, Player):
		msg = self.puzzle.solveAttempt(itemObj, Player)
		if self.puzzle.solved:
			self.hasPuzzle = False
			prizesNameList = []
			for item in self.puzzPrizes:
				BaseRoom.dropIntoRoom(self, item)
				prizesNameList.append(item.itemIDList[0])
			if len(prizesNameList) > 1:
				prizesNameStr = ', a '.join(prizesNameList[:-1])
				prizesNameStr += ', and a {}'.format(prizesNameList[-1])
			else:
				prizesNameStr = ''.join(prizesNameList)
			msg += '\nA {} appear in the room as a prize for solving the puzzle.'.format(prizesNameStr)
			self.directionsDict.update(self.lockedDirectionDict)
			self.lockedDirectionDict = {}
			self.descript += '\nThere is an unlocked door to the {}.'.format(self.fullDirection)
			return msg
		else:
			return msg

# Shop params = roomsDict, directionsDict, description, shopDict
class Shop(BaseRoom):
	def __init__(self, roomsDict, directionsDict, description, shopDict):
		self.directionsDict = directionsDict
		self.descript = description
		self.shopDict = shopDict
		self.roomID = 'shop'
		roomsDict[self.roomID] = self
		self.itemsList = []
		for key in self.shopDict:
			self.itemsList.append(key)
		self.roomsDict = roomsDict
		self.hasEnemy = False
		self.hasPuzzle = False
		self.isShop = True

	def hasItem(self, itemObj):
		return itemObj in self.shopDict

	def getPrice(self, itemObj):
		return self.shopDict[itemObj]

	def dropIntoRoom(self, item):
		return "You can't drop that here!"

	def takeFromRoom(self, item):
		return "That would be stealing, and a good Carl would never steal."

	def listWares(self):
		waresList = []
		for item in self.shopDict:
			str = '{}: {} schillers'.format(item.itemIDList[0], self.shopDict[item])
			waresList.append(str)
		waresStr = '\n'.join(waresList)
		return waresStr

	def describe(self):
		if self.shopDict != {}:
			waresStr = '-------\nSHOP WARES:\n-------\n'
			waresStr += self.listWares()
		else:
			waresStr = "You've bought everything!"
		return self.descript + '\n' + waresStr

	def buyFromShop(self, itemObj):
		del self.shopDict[itemObj]

# Player params = currentRoom
class Player:
	def __init__(self, startRoom):
		self.currentRoom = startRoom
		self.inventory = []
		self.HP = 100
		self.maxDamage = 6
		self.defense = 0
		# This will allow there to be multiple types of armor.
		self.armor = {'helmet': None, 'chest': None, 'legs': None, 'boots': None}
		self.weapon = None
		self.schillers = 0

	def isAlive(self):
		if self.HP > 0:
			return True
		else:
			return False

	def equipArmor(self, armor):
		armor.equipped = True
		if self.armor[armor.armorSlot] != None:
			self.armor[armor.armorSlot].equipped = False
			self.defense += armor.defense - self.armor[armor.armorSlot].defense
		else:
			self.defense += armor.defense
		self.armor[armor.armorSlot] = armor
		return 'Equipped.'

	def equipWeapon(self, weapon):
		self.maxDamage = 5 + weapon.damage
		weapon.equipped = True
		if self.weapon != None:
			self.weapon.equipped = False
		self.weapon = weapon
		return 'Equipped.'

	# METHODS THAT WILL BE CALLED BY INTERPRETER
	def getHP(self):
		return self.HP

	def move(self, direction):
		if self.currentRoom.isLegalMove(direction):
			newRoom = self.currentRoom.getMoveDestination(direction)
			self.currentRoom = newRoom
			return self.currentRoom.describe()
		else:
			return 'You can\'t go there!'

	def displayInventory(self):
		inventoryDesc = ['----', 'health: {}'.format(self.HP), 'damage: {}'.format(self.maxDamage), 'defense: {}'.format(self.defense), '----', '']
		for item in self.inventory:
			inventoryDesc.append(item.examine())
		inventoryDesc = '\n'.join(inventoryDesc)
		inventoryDesc += '\n{} SCHILLERS'.format(self.schillers)
		return inventoryDesc

	def take(self, item):
		if self.currentRoom.isShop:
			return "That would be stealing, and a good Carl would never steal."
		elif self.currentRoom.canTake(item) and item.moveable:
			if item.itemType == 'schillers':
				self.schillers += item.value
			else:
				self.inventory.append(item)
			self.currentRoom.takeFromRoom(item)
			if item.itemType == 'weapon' or item.itemType == 'armor':
				response = input('Would you like to equip {}? (Y/N) '.format(item.itemIDList[0]))
				while response[0].lower() != 'y' and response[0].lower() != 'n':
					response = input('Please enter a valid string. Would you like to equip {}? (Y/N) '.format(item.itemIDList[0]))
				if response[0].lower() == 'y':
					return self.equip(item)
			return 'Taken.'

		elif item in self.inventory:
			return 'You already have that item.'

		elif not item.moveable:
			return "You can't take that."

		else:
			return 'That item is not in this room.'

	def drop(self, item):
		if self.currentRoom.isShop:
			return "You can't drop that here!"
		if item in self.inventory:
			self.inventory.remove(item)
			self.currentRoom.dropIntoRoom(item)
			return 'Dropped.'
		else:
			return "You can't drop that item, it isn't in your inventory."

	def look(self):
		return self.currentRoom.describe()

	def equip(self, item):
		if item in self.inventory:
			if item.itemType == 'armor':
				return self.equipArmor(item)
			elif item.itemType == 'weapon':
				return self.equipWeapon(item)
			else:
				return 'This item cannot be equipped.'
		else:
			return 'You don\'t have one of those.'

	def attack(self):
		if self.currentRoom.hasEnemy:
			enemy = self.currentRoom.enemy
			msg = ''
			if enemy.isAlive() and self.isAlive():
				damage = int(random.gauss(self.maxDamage/2.0, 1.5))
				damMinusDef = damage - enemy.defense
				if damMinusDef < 0:
					damMinusDef = 0
				enemy.HP -= damMinusDef
				if damMinusDef > 0:
					if damage > self.maxDamage:
						msg += 'CRITICAL HIT! '
					msg += 'You attack {} for {} damage! {} has {} HP left.\n'.format(enemy.enemyID, damMinusDef, enemy.enemyID.capitalize(), enemy.HP)
				else:
					msg += 'You missed! {} has {} HP left.\n'.format(enemy.enemyID.capitalize(), enemy.HP)
			if enemy.isAlive() and self.isAlive():
				# Check again to see if both are alive
				msg += enemy.attackPlayer(self)
			if not self.isAlive():
				msg += '\nYOU HAVE DIED.'
			if not enemy.isAlive():
				msg += '\n' + enemy.die(self.currentRoom)
			return msg
		else:
			return 'There is nothing for you to attack in this room.'

	def examineItem(self, item):
		return item.examine()

	def useItem(self, itemObj):
		if self.currentRoom.hasPuzzle and (itemObj in self.currentRoom.itemsList or itemObj in self.inventory):
			if itemObj in self.inventory:
				noReturnMsg = self.currentRoom.dropIntoRoom(itemObj)
				if self.currentRoom.puzzle.puzType == 'order':
					itemObj.locDescript = self.currentRoom.puzzle.newLocDescript
				else:
					itemObj.locDescript = ''
			return self.currentRoom.puzzleSolve(itemObj, self)
		else:
			return "You can't use that here."

	def getHints(self):
		if self.currentRoom.hasPuzzle:
			return self.currentRoom.puzzle.getHints()
		else:
			return "There are no hints for this room."

	def buy(self, itemObj):
		if self.currentRoom.isShop:
			if self.currentRoom.hasItem(itemObj):
				price = self.currentRoom.getPrice(itemObj)
				if price > self.schillers:
					return "You don't have enough schillers."
				else:
					self.schillers -= price
					self.currentRoom.buyFromShop(itemObj)
					self.inventory.append(itemObj)
					if itemObj.itemType == 'weapon' or itemObj.itemType == 'armor':
						response = input('Would you like to equip {}? (Y/N) '.format(itemObj.itemIDList[0]))
						while response[0].lower() != 'y' and response[0].lower() != 'n':
							response = input('Please enter a valid string. Would you like to equip {}? (Y/N) '.format(itemObj.itemIDList[0]))
						if response[0].lower() == 'y':
							return self.equip(itemObj)

			else:
				return "The shop doesn't have that."
		else:
			return "You are not in a shop."

def helpString():
	helpMsg = '''
	----------------------------------------------
	| CARLETON COLLEGE: THE QUEST FOR SCHILLER   |
	| Written and programmed by Cayden Ehrlich   |
	| CS 111, Winter 2017                        |
	----------------------------------------------
Carleton College: The Quest For Schiller is a text-based adventure game. NOTE: although only some possible inputs are shown here, there are \
other logical inputs that will also work for these same commands (for example, instead of 'take <item>', \
you can also say 'grab <item>' or 'pick up <item>' to do the same thing.).

**COMMANDS**
'take <item>' or 't <item>': takes item from current room
'drop <item>' or 'd <item>': drops item into current room
'examine <item>' or 'x <item>': gives a description of the item
'equip <item>' or 'q <item>': equips a weapon or armor
'use <item>' or 'u <item>': uses an item
'buy <item>' or 'b <item>': buys an item from the shop (must be in the shop to use)

'look' or 'l': describes the current room
'attack' or 'a': attacks the monster in the current room (if there is one)
'health': displays how much health you have left
'inventory' or 'i': displays your inventory
'exit' or 'quit': closes game

'north' or 'n': moves north (if possible)
'south' or 's': moves south (if possible)
'east' or 'e': moves east (if possible)
'west' or 'w': moves west (if possible)

HINTS: Almost every object in the game is examinable, even if you can't pick it up! Try examining items; they may help you with puzzles!
'''
	return helpMsg

def createCommandDicts(Player):
	comWithArgsDict = {
		'take': Player.take, 'get': Player.take, 'grab': Player.take, 't': Player.take, 'pick up': Player.take,
		'drop': Player.drop, 'leave': Player.drop, 'd': Player.drop, 'put down': Player.take,
		'examine': Player.examineItem, 'x': Player.examineItem, 'look at': Player.examineItem,
		'equip': Player.equip, 'q': Player.equip,
		'use': Player.useItem, 'u': Player.useItem, 'throw': Player.useItem,
		'buy': Player.buy, 'b': Player.buy
	}

	comNoArgsDict = {
		'look': Player.look, 'describe': Player.look, 'l': Player.look,
		'attack': Player.attack, 'a': Player.attack,
		'health': Player.getHP, 'hp': Player.getHP,
		'inventory': Player.displayInventory, 'equipment': Player.displayInventory, 'i': Player.displayInventory,
		'help': helpString, 'h': helpString,
		'hint': Player.getHints, 'hints': Player.getHints
	}

	return comWithArgsDict, comNoArgsDict

def interpret(Player, itemsDict, playerCommand):
	'''NOTE: all of these return rather than print. This is because the function that actually runs the game will print the output of this function.'''
	comWithArgsDict, comNoArgsDict = createCommandDicts(Player)

	playerCommand = playerCommand.lower()
	splitCommand = playerCommand.split()
	directionsList = ['north', 'n', 'south', 's', 'east', 'e', 'west', 'w']
	twoWordComm = ' '.join(splitCommand[0:2])

	if Player.currentRoom.hasEnemy and not (playerCommand == 'attack' or playerCommand == 'a' or playerCommand == 'help'):
		'''If the player's current room has an enemy and the player tries to do something other than attack, they will get this
		message and the enemy will attack them.'''
		errorMsg = "You are in battle with an enemy! You can't do that!\n"
		errorMsg += Player.currentRoom.enemy.attackPlayer(Player)
		return errorMsg

	elif playerCommand in directionsList:
		'''If the command is a direction, the player will move in that direction.'''
		direction = playerCommand[0]
		return Player.move(direction)

	elif len(splitCommand) == 1:
		'''If the input is only one word and isn't direction, this will attempt to translate it using the dictionary of commands without arguments.
		If the input is a command that takes an argument, the program will ask for that argument.'''
		if playerCommand in comNoArgsDict:
			actualCommand = comNoArgsDict[splitCommand[0]]
			return actualCommand()
		elif playerCommand in comWithArgsDict:
			argStr = input('What would you like to ' + playerCommand + '? ')
			playerCommand += ' ' + argStr
			return interpret(Player, itemsDict, playerCommand)
		else:
			return "I don't understand."

	elif splitCommand[0] in comWithArgsDict:
		commandStr = splitCommand.pop(0)
		argStr = ' '.join(splitCommand)
		if argStr in ['schillers', 'gold', 'coins']:
			for item in Player.currentRoom.itemsList:
				if item.itemIDList[0] == 'schillers':
					arg = item
		else:
			if argStr in itemsDict:
				arg = itemsDict[argStr]
			else:
				return "I don't recognize that object."
		actualCommand = comWithArgsDict[commandStr]
		return actualCommand(arg)

	elif twoWordComm in comWithArgsDict:
		commandLst = [splitCommand.pop(0)]
		commandLst.append(splitCommand.pop(0))
		commandStr = ' '.join(commandLst)
		argStr = ' '.join(splitCommand)
		if argStr in ['schillers', 'gold', 'coins']:
			for item in Player.currentRoom.itemsList:
				if item.itemIDList[0] == 'schillers':
					arg = item
			else:
				if argStr in itemsDict:
					arg = itemsDict[argStr]
				else:
					return "I don't recognize that object."
		actualCommand = comWithArgsDict[commandStr]
		return actualCommand(arg)

	else:
		return "I don't recognize that command."

	'''elif splitCommand[-1] in itemsDict:
		argStr = splitCommand.pop()
		arg = itemsDict[argStr]
		commandStr = ' '.join(splitCommand)
		if commandStr in comWithArgsDict:
			actualCommand = comWithArgsDict[commandStr]
			return actualCommand(arg)
		else:
			return "I don't recognize that command."

	elif len(splitCommand) > 2 and twoWordNoun in itemsDict:
		argStr = twoWordNoun
		arg = itemsDict[argStr]
		commandStr = ' '.join(splitCommand[0:-2])
		if commandStr in comWithArgsDict:
			actualCommand = comWithArgsDict[commandStr]
			return actualCommand(arg)
		else:
			return "I don't recognize that command." '''

'''MAKE SURE TO INCLUDE DOORS IN ROOM DESCRIPTIONS'''

	# Item params = itemIDList, itemsDict, description, moveable, locDescript
	# Armor params = itemIDList, itemsDict, description, locDescript, defense, armorSlot
	# Weapon params = itemIDList, itemsDict, description, locDescript, damage
def game():
	itemsDict = {}
	roomsDict = {}
	enemiesList = []
	puzzlesList = []

	'''***ITEMS***'''
	# Schiller Room
	SchillerBust = Item(['schiller', 'bust', 'schiller bust'], itemsDict, 'The prized bust of Friedrich von Schiller.', True, 'The bust of Schiller sits on a pedestal in the middle of the room.')
	pedestal = Item(['pedestal'], itemsDict, 'A simple marble pedestal. It has an enscription, but you can\'t read it.', False, '')

	# BOSS ROOM
	schillers15 = Schillers(itemsDict, '', 15)

	# Laundry Room
	LaundryMaschine = Item(['laundry machine', 'machine', 'washing machine'], itemsDict, 'A basic laundry machine.', False, 'There\'s a small laundry machine in the middle of the room...')
	SPSword = Weapon(['Sword of Stevie P', 'sword of stevie p', 'sword of stevie p', 'sword', 'swordofsteviep'], itemsDict,
						'A glowing blade with "Steven Poskanzer" carved into the blade in calligraphy.', '', 15)

	# Levers Room
	lever1 = Item(['lever 1', '1', 'lever1'], itemsDict, 'A lever with the number "1" carved into the stone above it.', False, '')
	lever2 = Item(['lever 2', '2', 'lever2'], itemsDict, 'A lever with the number "2" carved into the stone above it.', False, '')
	lever3 = Item(['lever 3', '3', 'lever3'], itemsDict, 'A lever with the number "3" carved into the stone above it.', False, '')
	lever4 = Item(['lever 4', '4', 'lever4'], itemsDict, 'A lever with the number "4" carved into the stone above it.', False, '')
	lever5 = Item(['lever 5', '5', 'lever5'], itemsDict, 'A lever with the number "5" carved into the stone above it.', False, '')
	lever6 = Item(['lever 6', '6', 'lever6'], itemsDict, 'A lever with the number "6" carved into the stone above it.', False, '')
	lever7 = Item(['lever 7', '7', 'lever7'], itemsDict, 'A lever with the number "7" carved into the stone above it.', False, '')
	lever8 = Item(['lever 8', '8', 'lever8'], itemsDict, 'A lever with the number "8" carved into the stone above it.', False, '')
	lever9 = Item(['lever 9', '9', 'lever9'], itemsDict, 'A lever with the number "9" carved into the stone above it.', False, '')
	lever0 = Item(['lever 0', '0', 'lever0'], itemsDict, 'A lever with the number "0" carved into the stone above it.', False, '')

	frisbee = Item(['frisbee', 'disc', 'disk'], itemsDict, 'A Carleton College frisbee like the ones given to freshmen, but the year has been rubbed out.', True, 'There is a frisbee on the floor.')

	# Color Room
	table = Item(['table'], itemsDict, 'A simple wooden table.', False, 'There is a table in the center of the room.')
	maizeDisk = Item(['maize disk', 'gold disk', 'maize', 'maizedisk'], itemsDict, 'A maize disk.', True, 'There is a maize disk on the table.')
	greenDisk = Item(['green disk', 'green', 'greendisk'], itemsDict, 'A green disk.', True, 'There is a green disk on the table.')
	redDisk = Item(['red disk', 'red', 'reddisk'], itemsDict, 'A red disk.', True, 'There is a red disk on the table.')
	blueDisk = Item(['blue disk', 'blue', 'bluedisk'], itemsDict, 'A blue disk.', True, 'There is a blue disk on the table.')
	purpleDisk = Item(['purple disk', 'purple', 'purpledisk'], itemsDict, 'A purple disk.', True, 'There is a purple disk on the table.')

	schillers6 = Schillers(itemsDict, '', 6)

	# Workout Room
	KnightHelm = Armor(['knight helmet', 'knight helm', 'helmet', 'helm', 'knighthelmet', 'knighthelm'], itemsDict, 'A beautiful golden helmet with blue plumes; it resembles the knight on the \
		Carleton College logo', '', 8, 'helmet')

	# Furnace Room
	schillers10 = Schillers(itemsDict, '', 10)
	furnace = Item(['furnace'], itemsDict, 'It doesn\'t look like this furnace has had a fire in it for decades...it wouldn\'t be safe to use.', False, 'There\'s an old furnace in the corner.')

	# Shop
	# Armor params = itemIDList, itemsDict, description, locDescript, defense, armorSlot
	leatherCap = Armor(['leather cap', 'cap'], itemsDict, 'A simple leather cap.', '', 1, 'helmet')
	leatherTunic = Armor(['leather tunic', 'tunic'], itemsDict, 'A simple leather tunic.', '', 3, 'chest')
	leatherLeggings = Armor(['leather leggings', 'leggings'], itemsDict, 'Simple leather leggings.', '', 2, 'legs')
	leatherBoots = Armor(['leather boots', 'boots'], itemsDict, 'Simple leather boots.', '', 1, 'boots')

	# Prison
	dagger = Weapon(['dagger', 'dag'], itemsDict, 'A small, simple dagger.', '', 4)
	schillers8 = Schillers(itemsDict, '', 8)
	art = Item(['art'], itemsDict, 'A series of paintings that follow the life of Schiller from his birth to his death. They each say the year that the painting represents: the first one, of his \
birth, says 1759; the last, of his death, says 1805.', False, 'There\'s some art illustrating Schiller\'s life along the walls.')

	'''***ENEMIES***'''
	# Enemy params = enemyID, enemiesList, HP, damage, defense, itemsToDrop
	goblin = Enemy('small goblin', enemiesList, 8, 3, 0, [dagger, schillers8])
	skeleton = Enemy('spooky scary skeleton', enemiesList, 15, 4, 0, [schillers10])
	orc = Enemy('big smelly orc', enemiesList, 20, 10, 3, [SPSword])
	dragon = Enemy('giant dragon', enemiesList, 80, 25, 4, [schillers15])

	'''***PUZZLES***'''
	# SingleItemPuzzle params = puzzleDescription, victoryMsg, hintsList, puzzlesList, itemToUse
	# OrderPuzzle params = puzzleDescription, victoryMsg, hintsList, puzzlesList, objectOrderList, newLocDescript
	LeversPuzzle = OrderPuzzle('There are 10 levers lined up along the Eastern wall; they are numbered 0-9. There is an inscription above the levers - "Only true lovers of Schiller may find \
him, thus you must answer this question: In what year was Friedrich von Schiller born?"', 'You hear whirring behind the Eastern wall; it seems like you got the answer right!',
		['1. If only you knew more about Schiller\'s life...is there anything in this dungeon that could tell you?', '2. Art can tell you a lot about a person\'s life...', '3. Examine the art in the Prison.', '4. 1759'], puzzlesList, [lever1, lever7, lever5, lever9],
		'')
	MaizeAndBlue = OrderPuzzle('You see two circular indentations in the Northern wall, the same size as the disks on the table. An inscription on the wall above the indentations says in calligraphy: \
"O, Carleton, our Alma Mater, we hail the _____ and ____.', 'You did it! The Eastern door opens!', ['1. I wonder what Carleton\'s school colors are...', '2. Have you ever listened to the Carleton song?'
		'3. Maize and Blue.'], puzzlesList, [maizeDisk, blueDisk], 'is pressed into one of the indentations in the Northern wall.')
	Frisbro = SingleItemPuzzle('You see a huge orc dressed like a frisbee player in the middle of the room...they\'re way too big for you to fight. You\'ll have to get them out of the way somehow...',
		'The orc chases the frisbee across the room, until....SMACK! They run into a wall, knocking themselves unconscious.', ['1. Maybe you can distract them?', '2. What would distract a frisbee player?',
		'3. Use your frisbee.'], puzzlesList, frisbee)

	'''***ROOMS***'''
	# EnemyRoom params = roomID, roomsDict, directionsDict, description, itemsList, enemy
	# PuzzleRoom params = roomID, roomsDict, directionsDict, description, itemsList, puzzle, lockedDirectionDict, puzzPrizes
	# Shop params = roomsDict, directionsDict, description, shopDict

	firstRoom = BaseRoom('entrance', roomsDict, {'n': 'prison'}, 'A plain, stone room. There is a door to the North. Next to the door, written in red paint, are the words "ENTER IF YOU DARE".', [])
	prisonRoom = EnemyRoom('prison', roomsDict, {'s': 'entrance', 'n': 'levers room', 'e': 'shop'}, 'A dusty old prison. There are cobwebs everywhere. You can see all of the cell doors have broken open... \
There\'s a door to the South and a door to the North. The door to the Shop is to the East.', [art], goblin)
	shop = Shop(roomsDict, {'w': 'prison'}, 'You walk into a shop. There\'s a person working the register in the back...I wonder how long they\'ve been here?', {leatherCap: 2, leatherTunic: 6, leatherLeggings: 4, leatherBoots: 2})
	leversRoom = PuzzleRoom('levers room', roomsDict, {'s': 'prison'}, 'Another normal, basic room. This dungeon must be hundreds of years old... \nThere\'s a door to the South.', [lever0, lever1, lever2, lever3, lever4, lever5, lever6, lever7, lever8, lever9], LeversPuzzle, {'w': 'furnace room'}, [frisbee])
	furnaceRoom = EnemyRoom('furnace room', roomsDict, {'e': 'levers room', 'n': 'color room'}, 'You can see that this used to be where all of the coal was kept because of the dark black stains \
all over the floor. There\'s a door to the East and a door to the North.', [furnace], skeleton)
	colorRoom = PuzzleRoom('color room', roomsDict, {'s': 'furnace room'}, 'This room stands out from every other room. It has bright colors everywhere...it kind of hurts your eyes a little bit... \
There\'s a door to the South.', [table, maizeDisk, greenDisk, redDisk, blueDisk, purpleDisk], MaizeAndBlue, {'e': 'workout room'}, [schillers6])
	workoutRoom = PuzzleRoom('workout room', roomsDict, {'w': 'color room'}, 'Even monsters need to workout sometimes. There\'s a door to the West.', [], Frisbro, {'e': 'laundry room?'}, [KnightHelm])
	laundryRoom = EnemyRoom('laundry room?', roomsDict, {'w': 'workout room', 'n': 'BOSS ROOM'}, 'I guess monsters need to do laundry too? Who knows... There\'s a door to the West and a door to the \
North...you get the feeling that there\'s something dangerous to the North.', [LaundryMaschine], orc)
	bossRoom = EnemyRoom('BOSS ROOM', roomsDict, {'s': 'laundry room?', 'n': 'schiller room'}, 'This room seems to be plated in gold. There\'s a beautiful bejeweled door to the North and another one to the South. You look up and see...', [], dragon)
	schillerRoom = BaseRoom('schiller room', roomsDict, {'s': 'BOSS ROOM'}, 'You\'ve made it at last!', [pedestal, SchillerBust])

	player = Player(firstRoom)

	intro()
	print(player.look())
	playerCommand = input('>')
	while playerCommand not in ['exit', 'quit', '']:
		print(interpret(player, itemsDict, playerCommand))
		print()
		if not player.isAlive():
			print('YOU HAVE DIED.')
			break
		elif SchillerBust in player.inventory:
			print('VICTORY!!!!')
			break
		playerCommand = input('>')
	if playerCommand not in ['exit', 'quit', '']:
		print('Thank you for playing "Carleton College: The Quest for Schiller"! Would you like to play again? (Y/N)')
		response = input('>')
		while response[0].lower() not in ['y', 'n']:
			print("I don't understand. Do you want to play again? (Y/N)")
			response = input('>')
		if response[0].lower() == 'y':
			game()
	print('Goodbye!')

if __name__ == '__main__':
	game()