Big Bang
This commit is contained in:
commit
f871593191
22
index.html
Normal file
22
index.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Connect 4 - Garrett Mills | EECS 368</title>
|
||||
<link rel="stylesheet" href="./style/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Rivets binds to this. -->
|
||||
<div id="app-container">
|
||||
|
||||
<!-- The App component. -->
|
||||
<app-root></app-root>
|
||||
</div>
|
||||
|
||||
<!-- Libraries. -->
|
||||
<script src="lib/rivets.bundled.min.js"></script>
|
||||
|
||||
<!-- Start the application. -->
|
||||
<script src="src/index.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
6
lib/rivets.bundled.min.js
vendored
Normal file
6
lib/rivets.bundled.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
src/components.js
Normal file
18
src/components.js
Normal file
@ -0,0 +1,18 @@
|
||||
import App from './components/app.component.js'
|
||||
import Grid from './components/grid/grid.component.js'
|
||||
import GridCell from './components/grid/grid-cell.component.js'
|
||||
import GridRow from './components/grid/grid-row.component.js'
|
||||
import GridSlots from './components/grid/grid-slots.component.js'
|
||||
import Scoreboard from './components/scoreboard.component.js'
|
||||
|
||||
// Registry of components to be loaded by Rivets.js
|
||||
const components = {
|
||||
App, // the main app
|
||||
Grid, // the connect-4 grid
|
||||
GridCell, // a single cell in the connect-4 grid
|
||||
GridRow, // a row of cells in the connect-4 grid
|
||||
GridSlots, // the slots at the top of the connect-4 grid
|
||||
Scoreboard, // the game scoreboard
|
||||
}
|
||||
|
||||
export default components
|
33
src/components/app.component.js
Normal file
33
src/components/app.component.js
Normal file
@ -0,0 +1,33 @@
|
||||
import Component from '../rivets/Component.js'
|
||||
|
||||
// The root application component
|
||||
export default class App extends Component {
|
||||
static selector() { return 'app-root' }
|
||||
static template() { return `
|
||||
<h1>Connect 4 by Garrett Mills</h1>
|
||||
<p>This is a simple Connect-4 webtoy that I built as a project for EECS 368 at the University of Kansas. It's a two person game - take turns. Click the slot at the top of a column on the grid to drop the token on your turn.</p>
|
||||
<app-grid parent-app="self"></app-grid>
|
||||
<app-scoreboard parent-app="self"></app-scoreboard>
|
||||
<p><small>© { year } <a href="https://garrettmills.dev/">Garrett Mills</a> | <a href="#">Source Code</a></small></p>
|
||||
` }
|
||||
|
||||
// The current year for the copyright message
|
||||
year = ''
|
||||
|
||||
constructor(el, data) {
|
||||
super(el, data)
|
||||
|
||||
// Set the current year
|
||||
this.year = (new Date).getFullYear()
|
||||
}
|
||||
|
||||
// Callback for the grid creation. Gives access to the Grid class
|
||||
register_grid(grid) {
|
||||
this.grid = grid
|
||||
}
|
||||
|
||||
// Callback for the scoreboard creation. Gives access to the Scoreboard class
|
||||
register_scoreboard(scoreboard) {
|
||||
this.scoreboard = scoreboard
|
||||
}
|
||||
}
|
43
src/components/grid/grid-cell.component.js
Normal file
43
src/components/grid/grid-cell.component.js
Normal file
@ -0,0 +1,43 @@
|
||||
import Component from '../../rivets/Component.js'
|
||||
import { COLORS, PLAYER } from './grid.component.js'
|
||||
|
||||
// A single cell in the Connect-4 grid
|
||||
export default class GridCell extends Component {
|
||||
static selector() { return 'app-grid-cell' }
|
||||
static template() { return `
|
||||
<div class="cell-outer">
|
||||
<div class="cell-inner" rv-background="color"></div>
|
||||
</div>
|
||||
` }
|
||||
|
||||
constructor(el, data) {
|
||||
super(el, data)
|
||||
|
||||
// This cell's column number
|
||||
this.column_number = data.columnNumber;
|
||||
|
||||
// The player occupying this cell
|
||||
this.player = PLAYER.none;
|
||||
|
||||
// True if a player occupies this cell
|
||||
this.occupied = false;
|
||||
|
||||
// The color of this cell
|
||||
this.color = COLORS.empty;
|
||||
|
||||
this.parent_row = data.parentRow;
|
||||
this.parent_row.register_cell(this, this.column_number)
|
||||
}
|
||||
|
||||
// Change the player that occupies this cell - default none
|
||||
set_player(player = PLAYER.none) {
|
||||
this.player = player;
|
||||
this.occupied = player !== PLAYER.none;
|
||||
this.color = (player === PLAYER.one ? COLORS.player_1 : (player === PLAYER.two ? COLORS.player_2 : COLORS.empty))
|
||||
}
|
||||
|
||||
// Returns true if this cell is empty
|
||||
is_available() {
|
||||
return !this.occupied
|
||||
}
|
||||
}
|
37
src/components/grid/grid-row.component.js
Normal file
37
src/components/grid/grid-row.component.js
Normal file
@ -0,0 +1,37 @@
|
||||
import Component from '../../rivets/Component.js'
|
||||
import { PLAYER } from './grid.component.js'
|
||||
import { bindable } from '../../rivets/helpers.js'
|
||||
|
||||
// A row of cells in the Connect-4 grid
|
||||
export default class GridRow extends Component {
|
||||
static selector() { return 'app-grid-row' }
|
||||
static template() { return `
|
||||
<div class="grid-row-container">
|
||||
<app-grid-cell parent-row="self" column-number="0"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="1"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="2"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="3"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="4"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="5"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="6"></app-grid-cell>
|
||||
<app-grid-cell parent-row="self" column-number="7"></app-grid-cell>
|
||||
</div>
|
||||
` }
|
||||
|
||||
cells = Array(8).fill()
|
||||
|
||||
constructor(el, data) {
|
||||
super(el, data)
|
||||
|
||||
// This row's row-number
|
||||
this.row_number = data.rowNumber;
|
||||
|
||||
this.parent_grid = data.parentGrid;
|
||||
this.parent_grid.register_row(this, this.row_number);
|
||||
}
|
||||
|
||||
// Callback for cell creation - gives access to the cell component classes
|
||||
register_cell(cell_class, column_number) {
|
||||
this.cells[column_number] = cell_class;
|
||||
}
|
||||
}
|
41
src/components/grid/grid-slots.component.js
Normal file
41
src/components/grid/grid-slots.component.js
Normal file
@ -0,0 +1,41 @@
|
||||
import Component from '../../rivets/Component.js'
|
||||
import { bindable } from '../../rivets/helpers.js'
|
||||
|
||||
// Component for the slots at the top of the Connect-4 grid
|
||||
export default class GridSlots extends Component {
|
||||
static selector() { return 'app-grid-slots' }
|
||||
static template() { return `
|
||||
<div class="grid-slots-container">
|
||||
<div class="grid-slot-outer" rv-each-column="columns" rv-on-click="column.on_click">
|
||||
<div class="grid-slot-inner"></div>
|
||||
</div>
|
||||
</div>
|
||||
` }
|
||||
|
||||
columns = []
|
||||
|
||||
constructor(el, data) {
|
||||
const { nColumns, parentGrid } = data
|
||||
super(el, data)
|
||||
|
||||
this.parent_grid = parentGrid
|
||||
|
||||
// Populate the columns in this object
|
||||
this.columns = Array(8).fill().map((x,i) => this.column_state(i));
|
||||
}
|
||||
|
||||
// Called when a column is clicked - pass the event up
|
||||
on_column_clicked(column_number) {
|
||||
if ( this.parent_grid.allow_drop )
|
||||
this.parent_grid.on_drop(column_number)
|
||||
}
|
||||
|
||||
// Get the state object for a single column
|
||||
column_state(column_number) {
|
||||
return {
|
||||
on_click: () => {
|
||||
this.on_column_clicked(column_number)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
230
src/components/grid/grid.component.js
Normal file
230
src/components/grid/grid.component.js
Normal file
@ -0,0 +1,230 @@
|
||||
import Component from '../../rivets/Component.js'
|
||||
import { wait } from '../../rivets/helpers.js'
|
||||
|
||||
// Enum for the different players
|
||||
const PLAYER = {
|
||||
one: Symbol('one'),
|
||||
two: Symbol('two'),
|
||||
none: Symbol('none'),
|
||||
}
|
||||
|
||||
// Enum for some commonly used colors
|
||||
const COLORS = {
|
||||
empty: 'darkblue',
|
||||
player_1: '#f30000',
|
||||
player_2: '#daf500',
|
||||
victory: 'darkgreen',
|
||||
dark: '#333',
|
||||
}
|
||||
|
||||
export { PLAYER, COLORS }
|
||||
|
||||
// Component for the Connect-4 grid
|
||||
export default class Grid extends Component {
|
||||
static selector() { return 'app-grid' }
|
||||
static template() { return `
|
||||
<app-grid-slots parent-grid="self"></app-grid-slots>
|
||||
<app-grid-row parent-grid="self" row-number="0"></app-grid-row>
|
||||
<app-grid-row parent-grid="self" row-number="1"></app-grid-row>
|
||||
<app-grid-row parent-grid="self" row-number="2"></app-grid-row>
|
||||
<app-grid-row parent-grid="self" row-number="3"></app-grid-row>
|
||||
<app-grid-row parent-grid="self" row-number="4"></app-grid-row>
|
||||
<app-grid-row parent-grid="self" row-number="5"></app-grid-row>
|
||||
` }
|
||||
|
||||
// Array of GridRow components
|
||||
rows = Array(6).fill()
|
||||
|
||||
// GridSlots only allows drop if this is true
|
||||
allow_drop = true
|
||||
|
||||
// The current player
|
||||
current_player = PLAYER.one
|
||||
|
||||
// The designated winner
|
||||
winner = undefined
|
||||
|
||||
constructor(el, data) {
|
||||
super(el, data)
|
||||
|
||||
this.app = data.parentApp
|
||||
this.app.register_grid(this)
|
||||
}
|
||||
|
||||
on_drop(column_number) {
|
||||
// Check if a drop is possible on the column
|
||||
if ( this._can_drop_on_column(column_number) ) {
|
||||
|
||||
// Disallow drops temporarily
|
||||
this.allow_drop = false
|
||||
|
||||
// Wrap async so we can use wait non-blocking
|
||||
;(async () => {
|
||||
// Animate the move so there's an actual 'drop'
|
||||
for ( let i = 0; i < this.rows.length; i++ ) {
|
||||
const cell = this.rows[i].cells[column_number]
|
||||
await wait(20)
|
||||
if ( cell.is_available() ) {
|
||||
if ( i > 0 ) this.rows[i - 1].cells[column_number].set_player()
|
||||
cell.set_player(this.current_player)
|
||||
await wait(20)
|
||||
} else break // stop if we hit the end, or this row has a chip already
|
||||
}
|
||||
|
||||
// Check for a winner
|
||||
const winner = this.get_winner()
|
||||
if ( winner ) {
|
||||
this.winner = winner
|
||||
this.on_game_over()
|
||||
return
|
||||
}
|
||||
|
||||
// Check for full grid
|
||||
if ( this.is_full() ) {
|
||||
this.winner = PLAYER.none
|
||||
this.on_game_over()
|
||||
return
|
||||
}
|
||||
|
||||
// Unblock dropping and switch to the other player
|
||||
this.allow_drop = true
|
||||
this.change_player()
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
// Callback from row creation - register the rows' component classes
|
||||
register_row(row, row_number) {
|
||||
this.rows[row_number] = row;
|
||||
}
|
||||
|
||||
// Change the current player (e.g. P1 -> P2/P2 -> P1)
|
||||
change_player() {
|
||||
if ( this.current_player === PLAYER.one ) this.current_player = PLAYER.two
|
||||
else this.current_player = PLAYER.one
|
||||
}
|
||||
|
||||
// Clears the grid and resets the game to play again
|
||||
reset() {
|
||||
this.get_cells().some(cell => cell.set_player())
|
||||
this.winner = undefined
|
||||
this.current_player = PLAYER.one
|
||||
this.allow_drop = true
|
||||
}
|
||||
|
||||
// Get a flat array of all the grid's cells
|
||||
get_cells() {
|
||||
return this.rows.map(row => row.cells).flat()
|
||||
}
|
||||
|
||||
// Get the cells in groups of columns, not rows
|
||||
get_columns() {
|
||||
return Array(8).fill().map((x,i) => this.rows.map(row => row.cells[i]))
|
||||
}
|
||||
|
||||
// Get all sets of 4 diagonal cells
|
||||
get_diagonals() {
|
||||
const columns = this.get_columns()
|
||||
const diagonal_groups = []
|
||||
|
||||
// Build the upward-right diagonals (possible from cell 3 on columns 0 -> 4)
|
||||
const up_right_cols = columns.slice(0, 5);
|
||||
up_right_cols.some((col, i) => {
|
||||
for ( const cell_index of [3, 4, 5] ) {
|
||||
const diagonal_group = [
|
||||
col[cell_index],
|
||||
columns[i + 1][cell_index - 1],
|
||||
columns[i + 2][cell_index - 2],
|
||||
columns[i + 3][cell_index - 3],
|
||||
]
|
||||
|
||||
diagonal_groups.push(diagonal_group)
|
||||
}
|
||||
})
|
||||
|
||||
// Build the upward-left diagonals (possible from cell 3 on columns 3 -> 7)
|
||||
const up_left_cols = columns.slice(3, 8);
|
||||
up_left_cols.some((col, ind) => {
|
||||
// Correct the index with respect to this.columns (because we sliced from the front)
|
||||
const i = ind + 3;
|
||||
|
||||
for ( const cell_index of [3, 4, 5] ) {
|
||||
const diagonal_group = [
|
||||
col[cell_index],
|
||||
columns[i - 1][cell_index - 1],
|
||||
columns[i - 2][cell_index - 2],
|
||||
columns[i - 3][cell_index - 3],
|
||||
]
|
||||
|
||||
diagonal_groups.push(diagonal_group)
|
||||
}
|
||||
})
|
||||
|
||||
return diagonal_groups
|
||||
}
|
||||
|
||||
// True if the grid is full, false otherwise
|
||||
is_full() {
|
||||
return Array(8).fill().every((x, i) => !this._can_drop_on_column(i))
|
||||
}
|
||||
|
||||
// Returns true if a drop is possible on the given column number
|
||||
_can_drop_on_column(column_number) {
|
||||
return this.rows[0].cells[column_number].is_available()
|
||||
}
|
||||
|
||||
// Check if either of the players has won the game
|
||||
// If they have, return the winner, otherwise return undefined
|
||||
get_winner() {
|
||||
// Check if there are any row-wise runs of 4
|
||||
const row_wise = this.rows.map(row => this._check_cells_for_winner(row.cells))
|
||||
.filter(player => player !== PLAYER.none)
|
||||
.shift()
|
||||
|
||||
// Check if there are any column-wise runs of 4
|
||||
const column_wise = this.get_columns()
|
||||
.map(col_group => this._check_cells_for_winner(col_group))
|
||||
.filter(player => player !== PLAYER.none)
|
||||
.shift()
|
||||
|
||||
// Check if there are any diagonal runs of 4
|
||||
const diagonal_wise = this.get_diagonals()
|
||||
.map(diag_group => this._check_cells_for_winner(diag_group))
|
||||
.filter(player => player !== PLAYER.none)
|
||||
.shift()
|
||||
|
||||
return [row_wise, column_wise, diagonal_wise].filter(Boolean).shift()
|
||||
}
|
||||
|
||||
// Check the group of cells for a 4-long run
|
||||
// Returns the player with the run, or PLAYER.none if no run was found
|
||||
_check_cells_for_winner(cells = []) {
|
||||
let current_player = PLAYER.none
|
||||
let run_length = 0
|
||||
|
||||
for ( const cell of cells ) {
|
||||
if ( cell.is_available() ) {
|
||||
current_player = PLAYER.none
|
||||
run_length = 0
|
||||
} else if ( cell.player === current_player ) {
|
||||
run_length += 1;
|
||||
if ( run_length >= 4 ) return current_player
|
||||
} else {
|
||||
current_player = cell.player
|
||||
run_length = 1
|
||||
}
|
||||
}
|
||||
|
||||
return PLAYER.none
|
||||
}
|
||||
|
||||
// Fired when the game has ended, either from a winner or a draw
|
||||
on_game_over() {
|
||||
;(async () => {
|
||||
if ( this.winner !== PLAYER.none ) {
|
||||
await this.app.scoreboard.increment(this.winner)
|
||||
this.reset()
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
71
src/components/scoreboard.component.js
Normal file
71
src/components/scoreboard.component.js
Normal file
@ -0,0 +1,71 @@
|
||||
import Component from '../rivets/Component.js'
|
||||
import { PLAYER, COLORS } from './grid/grid.component.js'
|
||||
import { wait } from '../rivets/helpers.js'
|
||||
|
||||
// The scoreboard for the Connect-4 games
|
||||
export default class Scoreboard extends Component {
|
||||
static selector() { return 'app-scoreboard' }
|
||||
static template() { return `
|
||||
<div class="scoreboard-container">
|
||||
<div class="player" rv-background="player_one_bkg">
|
||||
<p class="score">{ player_one }</p>
|
||||
<p class="name">Player One</p>
|
||||
</div>
|
||||
<div class="player" rv-background="player_two_bkg">
|
||||
<p class="score">{ player_two }</p>
|
||||
<p class="name">Player Two</p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button rv-on-click="bind.on_board_reset_click">Reset Board</button>
|
||||
<button rv-on-click="bind.on_score_reset_click">Reset Score</button>
|
||||
</div>
|
||||
</div>
|
||||
` }
|
||||
|
||||
// The players' scores
|
||||
player_one = 0
|
||||
player_two = 0
|
||||
|
||||
// The players' background colors
|
||||
player_one_bkg = COLORS.dark
|
||||
player_two_bkg = COLORS.dark
|
||||
|
||||
constructor(el, data) {
|
||||
super(el, data)
|
||||
|
||||
this.app = data.parentApp
|
||||
this.app.register_scoreboard(this)
|
||||
}
|
||||
|
||||
// Increment the specified player's score, animating the background for a few seconds
|
||||
async increment(player) {
|
||||
if ( player === PLAYER.one ) {
|
||||
this.player_one_bkg = COLORS.victory
|
||||
this.player_one += 1
|
||||
await wait(3000)
|
||||
this.player_one_bkg = COLORS.dark
|
||||
} else if ( player === PLAYER.two ) {
|
||||
this.player_two_bkg = COLORS.victory
|
||||
this.player_two += 1
|
||||
await wait(3000)
|
||||
this.player_two_bkg = COLORS.dark
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the score
|
||||
reset() {
|
||||
this.player_one = 0
|
||||
this.player_two = 0
|
||||
}
|
||||
|
||||
// Called when the "Reset Board" button is clicked
|
||||
on_board_reset_click() {
|
||||
this.app.grid.reset()
|
||||
}
|
||||
|
||||
// Called when the "Reset Score" button is clicked
|
||||
on_score_reset_click() {
|
||||
this.app.grid.reset()
|
||||
this.reset()
|
||||
}
|
||||
}
|
11
src/index.js
Normal file
11
src/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import components from './components.js'
|
||||
import Loader from './rivets/Loader.js'
|
||||
|
||||
// Create the framework loader
|
||||
const loader = new Loader({ components })
|
||||
|
||||
// Register the components
|
||||
loader.initialize()
|
||||
|
||||
// Bind the application to the app-container
|
||||
loader.bind('#app-container')
|
57
src/rivets/Component.js
Normal file
57
src/rivets/Component.js
Normal file
@ -0,0 +1,57 @@
|
||||
import { bindable } from './helpers.js'
|
||||
|
||||
// Base class for a Rivets.js component
|
||||
export default class Component {
|
||||
|
||||
/**
|
||||
* return the HTML selector of the component
|
||||
* @return {string}
|
||||
*/
|
||||
static selector() { return '' }
|
||||
|
||||
/**
|
||||
* return the HTML template of the component
|
||||
* @return {string}
|
||||
*/
|
||||
static template() { return '' }
|
||||
|
||||
/**
|
||||
* Called when the component is initialized
|
||||
* @param {element} el - the instantiated DOM element
|
||||
* @param {object} data - the data attributes of the component
|
||||
* @return {Component}
|
||||
*/
|
||||
static initialize(el, data) {
|
||||
return new this(el, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
* @param {element} el - the instantiated DOM element
|
||||
* @param {object} data - the data attributes of the component
|
||||
*/
|
||||
constructor(el, data) {
|
||||
|
||||
/**
|
||||
* The DOM element this component is associated with.
|
||||
* @type {element}
|
||||
*/
|
||||
this.element = el
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reference to this component.
|
||||
* Useful for passing the whole component along.
|
||||
*/
|
||||
get self() {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a self-binding proxy wrapper for this class.
|
||||
* Useful for passing methods to components.
|
||||
*/
|
||||
get bind() {
|
||||
return bindable(this)
|
||||
}
|
||||
}
|
41
src/rivets/Loader.js
Normal file
41
src/rivets/Loader.js
Normal file
@ -0,0 +1,41 @@
|
||||
// Loads the assets/components into the Rivets.js framework
|
||||
export default class Loader {
|
||||
constructor({ components }) {
|
||||
this.components = components
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
// Prepare the Rivets.js framework by loading component definitions
|
||||
initialize() {
|
||||
for ( const key in this.components ) {
|
||||
if ( !this.components.hasOwnProperty(key) ) continue
|
||||
const component = this.components[key]
|
||||
|
||||
// Register the component class with the rivets framework
|
||||
rivets.components[component.selector()] = {
|
||||
template: () => {
|
||||
return component.template()
|
||||
},
|
||||
|
||||
initialize: (el, data) => {
|
||||
return new component(el, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the custom property binders
|
||||
this._init_binders()
|
||||
}
|
||||
|
||||
_init_binders() {
|
||||
// Binds to the CSS background property
|
||||
rivets.binders.background = (el, value) => {
|
||||
el.style.background = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the Rivets.js application to the specified target
|
||||
bind(target) {
|
||||
rivets.bind(document.querySelector(target), this.state)
|
||||
}
|
||||
}
|
26
src/rivets/helpers.js
Normal file
26
src/rivets/helpers.js
Normal file
@ -0,0 +1,26 @@
|
||||
// Wraps an object-like thing with a proxy to self-bind top-level functions
|
||||
const bindable = (base_class) => {
|
||||
return new Proxy(base_class, {
|
||||
get(target, property) {
|
||||
// If we're accessing a function of the item, force-bind it
|
||||
if ( typeof base_class[property] === 'function' && base_class.hasOwnProperty(property) ) {
|
||||
return base_class[property].bind(base_class)
|
||||
}
|
||||
|
||||
return base_class[property]
|
||||
},
|
||||
|
||||
set(target, property, value) {
|
||||
base_class[property] = value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Async sleeper function
|
||||
const wait = (ms) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
||||
|
||||
export { bindable, wait }
|
84
style/index.css
Normal file
84
style/index.css
Normal file
@ -0,0 +1,84 @@
|
||||
@media only screen and (min-width: 1200px) {
|
||||
#app-container {
|
||||
max-width: 50%;
|
||||
margin-left: 25%;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
app-grid-cell .cell-outer {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
background-color: #2222cc;
|
||||
}
|
||||
|
||||
app-grid-cell .cell-outer .cell-inner {
|
||||
width: 69px;
|
||||
height: 69px;
|
||||
margin: 3px;
|
||||
background: darkblue;
|
||||
position: absolute;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
app-grid-row .grid-row-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
app-grid-slots .grid-slots-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
app-grid-slots .grid-slots-container .grid-slot-outer {
|
||||
width: 75px;
|
||||
height: 20px;
|
||||
background-color: #2222cc;
|
||||
transform: skew(-15deg) translateX(3px);
|
||||
}
|
||||
|
||||
app-grid-slots .grid-slots-container .grid-slot-outer .grid-slot-inner {
|
||||
width: 69px;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
background: darkblue;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
app-scoreboard .scoreboard-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
app-scoreboard .scoreboard-container .player {
|
||||
background: #333;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
margin: 7px;
|
||||
border-radius: 7px;
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
app-scoreboard .scoreboard-container .player .score {
|
||||
font-size: 40px;
|
||||
line-height: 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
app-scoreboard .scoreboard-container .buttons {
|
||||
display: inline-grid;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
app-scoreboard .scoreboard-container .buttons button {
|
||||
margin: 3px;
|
||||
padding: 5px 20px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
app-scoreboard .scoreboard-container .buttons button:hover {
|
||||
color: #ddd;
|
||||
background: #333;
|
||||
}
|
Loading…
Reference in New Issue
Block a user