Open Source commit

main
gabe farrell 4 years ago
commit 7ad332ad02

3
.gitignore vendored

@ -0,0 +1,3 @@
design
massflip.exe
.env

@ -0,0 +1,6 @@
# Massflip
Massflip is an online coinflip betting game using virtual currency called GP. It has an account system, realtime chat, and realtime betting.
## Technology
Massflip is built using a Go backend, a Vue.js composition API frontend, and a Mongo database to handle accounts.
![UI_idea_2](https://user-images.githubusercontent.com/101588735/175800419-cf7ba34d-503f-4672-acf4-79125110bba1.png)

@ -0,0 +1,276 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"golang.org/x/crypto/bcrypt"
)
type Credentials struct {
Username string `json:"username" bson:"username"`
Password string `json:"password" bson:"password"`
}
type Login struct {
Username string `json:"username" bson:"username"`
Password string `json:"password" bson:"password"`
RememberMe bool `json:"remember"`
}
type Session struct {
Session string `json:"session" bson:"session"`
}
// ExistingAccount is a struct that mirrors how Users are stored in the Database
type ExistingAccount struct {
Username string `json:"username" bson:"username"`
Password string `json:"password" bson:"password"`
Case string `json:"case" bson:"case"`
Color string `json:"color" bson:"color"`
Points int `json:"points" bson:"points"`
Resets int `json:"resets" bson:"resets"`
Session string `json:"session" bson:"session"`
}
type ReturnedAccount struct {
Username string `json:"username" bson:"case"`
Color string `json:"color" bson:"color"`
Points int `json:"points" bson:"points"`
Resets int `json:"resets" bson:"resets"`
}
var (
UsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9-_]{3,24}$`)
)
func createAccount(w http.ResponseWriter, r *http.Request) {
// prepare DB
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
// var v contains POST credentials
var v Login
err = json.NewDecoder(r.Body).Decode(&v)
if err != nil || len(v.Password) < 8 || len(v.Password) > 255 || !UsernameRegex.MatchString(v.Username) {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, "{\"error\":\"there was a problem with your request. Please try again with different values\"}")
return
}
// search if user with that username already exists
find := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}})
if find.Err() == nil {
w.WriteHeader(http.StatusConflict)
fmt.Fprint(w, "{\"error\":\"user already exists with that username\"}")
return
}
// create a new session for the new user
sessionID := uuid.NewString()
var session *http.Cookie
if v.RememberMe {
expire := time.Now().Add(30 * 24 * time.Hour)
session = &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 0,
Expires: expire,
}
} else {
session = &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 0,
}
}
http.SetCookie(w, session)
// hash and store the user's hashed password
hasedPass, err := bcrypt.GenerateFromPassword([]byte(v.Password), 8)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "{\"error\":\"internal server error, please try again later\"}")
}
// add the new user to the database
var acc ExistingAccount
acc.Username = strings.ToLower(v.Username)
acc.Password = string(hasedPass)
acc.Case = v.Username
acc.Color = "white"
acc.Points = 100
acc.Resets = 0
acc.Session = sessionID
_, err = userCollection.InsertOne(r.Context(), acc)
if err != nil {
log.Println("* Error inserting new user")
}
// return the account information to the user
var ret ReturnedAccount
ret.Username = v.Username
ret.Color = "white"
ret.Points = 100
ret.Resets = 0
account, err := json.Marshal(ret)
if err != nil {
fmt.Println("* Error marshalling bson.D response")
}
fmt.Fprint(w, string(account))
}
func login(w http.ResponseWriter, r *http.Request) {
// prepare DB collection
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
w.Header().Set("Content-Type", "application/json")
userCollection := DB.Database("Users").Collection("Users")
// decode POST into v struct
var v Login
json.NewDecoder(r.Body).Decode(&v)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "{\"error\":\"bad request. Try again later\"}")
return
}
// cmp struct will be compared with v to verify credentials
var cmp Login
found := userCollection.FindOne(r.Context(), bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}})
if found.Err() != nil {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, "{\"error\":\"account with that username does not exist\"}")
return
}
err = found.Decode(&cmp)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
err = bcrypt.CompareHashAndPassword([]byte(cmp.Password), []byte(v.Password))
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, "{\"error\":\"invalid password\"}")
return
}
// prepare ReturnedAccount struct to be sent back to the client
var account ReturnedAccount
err = found.Decode(&account)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// set new session cookie for user, either persistent (remember me) or temporary
sessionID := uuid.NewString()
var session *http.Cookie
if v.RememberMe {
expire := time.Now().Add(30 * 24 * time.Hour)
session = &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 0,
Expires: expire,
}
} else {
session = &http.Cookie{
Name: "session",
Value: sessionID,
Path: "/",
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: 0,
}
}
http.SetCookie(w, session)
// update the new user session in the DB
filter := bson.D{primitive.E{Key: "username", Value: strings.ToLower(account.Username)}}
opts := options.Update().SetUpsert(true)
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "session", Value: sessionID}}}}
userCollection.UpdateOne(context.TODO(), filter, update, opts)
acc, err := json.Marshal(account)
if err != nil {
fmt.Println("Error marshalling bson.D response")
}
fmt.Fprint(w, string(acc))
//fmt.Println("logged in user successfully")
}
func loginBySession(w http.ResponseWriter, r *http.Request) {
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
var id Session
var account ReturnedAccount
json.NewDecoder(r.Body).Decode(&id)
filter := bson.D{primitive.E{Key: "session", Value: id.Session}}
find := userCollection.FindOne(r.Context(), filter)
if find.Err() != nil {
log.Println(find.Err())
fmt.Fprintf(w, "{\"error\":\"no user with given session id\"}")
return
}
err = find.Decode(&account)
if err != nil {
log.Println(err)
fmt.Fprintf(w, "{\"error\":\"cannot decode user bson from session\"}")
return
}
json.NewEncoder(w).Encode(account)
}
func logout(w http.ResponseWriter, r *http.Request) {
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
var v Credentials
json.NewDecoder(r.Body).Decode(&v)
filter := bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}}
opts := options.Update().SetUpsert(true)
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "session", Value: ""}}}}
userCollection.UpdateOne(context.TODO(), filter, update, opts)
}

@ -0,0 +1,43 @@
package main
import (
"context"
"encoding/json"
"net/http"
"strings"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
type ColorRequest struct {
Username string `json:"username" bson:"username"`
Color string `json:"color" bson:"color"`
}
func chatColor(w http.ResponseWriter, r *http.Request) {
// prepare DB
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
// decode PUT into v struct
var v ColorRequest
json.NewDecoder(r.Body).Decode(&v)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
filter := bson.D{primitive.E{Key: "username", Value: strings.ToLower(v.Username)}}
opts := options.Update().SetUpsert(true)
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "color", Value: v.Color}}}}
_, err = userCollection.UpdateOne(context.TODO(), filter, update, opts)
if err != nil {
return
}
}

@ -0,0 +1,198 @@
package main
import (
"encoding/json"
"fmt"
"log"
"strconv"
"time"
)
// Chat Hub code adapted from https://github.com/gorilla/websocket/tree/master/examples/chat
// Hub maintains the set of active clients and broadcasts messages to the
// clients.
type Hub struct {
// Registered clients.
clients map[*Client]bool
// Inbound messages from the clients.
broadcast chan []byte
// Register requests from the clients.
register chan *Client
// Unregister requests from clients.
unregister chan *Client
// Betting Hub Data
headsPool int
headsBetters map[string]int
tailsPool int
tailsBetters map[string]int
allUsers map[string]string
largestBet int
}
type WsMessage struct {
Type string `json:"type"`
Username string `json:"username,omitempty"`
}
type WsBetMessage struct {
Type string `json:"type"`
Username string `json:"username"`
Bet string `json:"bet"`
Amount int `json:"amount"`
}
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
headsBetters: make(map[string]int),
tailsBetters: make(map[string]int),
allUsers: make(map[string]string),
}
}
func (h *Hub) broadcastPoolUpdate() {
var msg string
heads := strconv.Itoa(len(h.headsBetters))
tails := strconv.Itoa(len(h.tailsBetters))
headspool := strconv.Itoa(h.headsPool)
tailspool := strconv.Itoa(h.tailsPool)
msg = "{\"type\":\"pool\",\"heads\":" + heads + ",\"tails\":" + tails + ",\"headspool\":" + headspool + ",\"tailspool\":" + tailspool + "}"
h.broadcast <- []byte(msg)
}
func (h *Hub) tick(s int) {
var msg string
heads := strconv.Itoa(len(h.headsBetters))
tails := strconv.Itoa(len(h.tailsBetters))
headspool := strconv.Itoa(h.headsPool)
tailspool := strconv.Itoa(h.tailsPool)
msg = "{\"type\":\"tick\",\"clock\":" + strconv.Itoa(s) + ",\"heads\":" + heads + ",\"tails\":" + tails + ",\"headspool\":" + headspool + ",\"tailspool\":" + tailspool + "}"
h.broadcast <- []byte(msg)
}
func (h *Hub) processMessage(message []byte) error {
var msg WsMessage
err := json.Unmarshal(message, &msg)
if err != nil {
// need error handling
fmt.Println("* Error unmarshalling WebSocket message")
log.Println(string(message))
}
if msg.Type == "bet" {
var betMsg WsBetMessage
err := json.Unmarshal(message, &betMsg)
if err != nil {
fmt.Println("* Error unmarshalling WebSocket bet message")
}
if h.allUsers[betMsg.Username] != "" {
log.Println("* Disallowed bet from user " + betMsg.Username + ": You can't bet twice")
return fmt.Errorf("cant bet twice")
}
if betMsg.Amount > DBGetUserByUsername(betMsg.Username).Points || betMsg.Amount <= 0 {
log.Println("* Disallowed bet from user " + betMsg.Username + ": Cannot bet more gp than you have or bet 0")
return fmt.Errorf("cant bet more gp than you have")
}
err = DBSubtractPoints(betMsg.Username, betMsg.Amount)
if err != nil {
log.Println("* Error subtracting points from user " + betMsg.Username + " in DB")
}
if betMsg.Bet == "heads" {
h.allUsers[betMsg.Username] = "heads"
h.headsBetters[betMsg.Username] = betMsg.Amount
h.headsPool += betMsg.Amount
fmt.Println("* " + betMsg.Username + " has bet " + strconv.Itoa(h.headsBetters[betMsg.Username]) + " on heads")
} else if betMsg.Bet == "tails" {
h.allUsers[betMsg.Username] = "tails"
h.tailsBetters[betMsg.Username] = betMsg.Amount
h.tailsPool += betMsg.Amount
fmt.Println("* " + betMsg.Username + " has bet " + strconv.Itoa(h.tailsBetters[betMsg.Username]) + " on tails")
}
h.broadcastPoolUpdate()
} else if msg.Type == "flip" {
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
// this 50ms delay prevents the WS messages from colliding when they reach
// the client and causing a json parsing error
time.Sleep(50 * time.Millisecond)
var v struct {
Type string `json:"type"`
Value string `json:"value"`
}
err := json.Unmarshal(message, &v)
if err != nil {
fmt.Println("* Error unmarshalling WebSocket bet message")
}
if h.headsPool+h.tailsPool == 0 {
//fmt.Println("* No betters in pool, skipping payout")
return fmt.Errorf("no betters in pool")
}
for c := range h.clients {
if h.headsBetters[c.username] > 0 && v.Value == "heads" {
winAmt := int(float64(h.headsBetters[c.username])/float64(h.headsPool)*float64(h.tailsPool)) + h.headsBetters[c.username]
m := "{\"type\":\"win\",\"value\":" + strconv.Itoa(winAmt) + "}"
c.send <- []byte(m)
DBAddPoints(c.username, winAmt)
fmt.Println("* " + strconv.Itoa(winAmt) + " paid out to " + c.username)
} else if h.tailsBetters[c.username] > 0 && v.Value == "tails" {
winAmt := int(float64(h.tailsBetters[c.username])/float64(h.tailsPool)*float64(h.headsPool)) + h.tailsBetters[c.username]
m := "{\"type\":\"win\",\"value\":" + strconv.Itoa(winAmt) + "}"
c.send <- []byte(m)
DBAddPoints(c.username, winAmt)
fmt.Println("* " + strconv.Itoa(winAmt) + " paid out to " + c.username)
}
}
h.headsBetters = make(map[string]int)
h.tailsBetters = make(map[string]int)
h.allUsers = make(map[string]string)
h.headsPool = 0
h.tailsPool = 0
h.largestBet = 0
// reset pool for clients
} else {
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
return nil
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
go h.processMessage(message)
}
}
}

@ -0,0 +1,149 @@
package main
import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
"github.com/gorilla/websocket"
)
// code adapted from the gorilla websocket chat demo code
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 60 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 512
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// Client is a middleman between the websocket connection and the hub.
type Client struct {
hub *Hub
// The websocket connection.
conn *websocket.Conn
// Buffered channel of outbound messages.
send chan []byte
username string
}
// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
var v WsBetMessage
err = json.Unmarshal(message, &v)
if err == nil {
if v.Type == "bind" {
c.username = strings.ToLower(v.Username)
if c.hub.allUsers[c.username] != "" {
c.send <- []byte("{\"type\":\"hasbet\",\"value\":true,\"bet\":\"" + c.hub.allUsers[c.username] + "\"}")
}
continue
}
}
c.hub.broadcast <- message
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// serveWs handles websocket requests from the peer.
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client.writePump()
go client.readPump()
}

@ -0,0 +1,31 @@
package main
import (
"math/rand"
"time"
)
const (
// time until next flip
GAME_TIME = 180
)
func (h *Hub) runGameClock() {
var msg string
for {
for i := GAME_TIME; i >= 0; i-- {
h.tick(i)
time.Sleep(time.Second)
}
HorT := rand.Int() % 2
if HorT == 1 {
msg = "{\"type\":\"flip\",\"value\":\"heads\"}"
} else {
msg = "{\"type\":\"flip\",\"value\":\"tails\"}"
}
h.broadcast <- []byte(msg)
time.Sleep(4 * time.Second)
h.broadcastPoolUpdate()
time.Sleep(1 * time.Second)
}
}

106
db.go

@ -0,0 +1,106 @@
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
)
var DB = openDB()
func openDB() *mongo.Client {
err := godotenv.Load()
if err != nil {
log.Fatal("Unable to load database. Shutting down...")
}
dbUsername := os.Getenv("DB_USERNAME")
dbPassword := os.Getenv("DB_PASSWORD")
serverAPIOptions := options.ServerAPI(options.ServerAPIVersion1)
clientOptions := options.Client().
ApplyURI("mongodb+srv://" + dbUsername + ":" + dbPassword + "@cluster0.tqrat.mongodb.net/myFirstDatabase?retryWrites=true&w=majority").
SetServerAPIOptions(serverAPIOptions)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
client, err := mongo.Connect(ctx, clientOptions)
err = client.Ping(ctx, readpref.Primary())
if err != nil {
log.Fatal("DB Error: " + err.Error())
}
return client
}
func DBSubtractPoints(user string, p int) error {
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
var v ExistingAccount
err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v)
if err != nil {
fmt.Println("* Error editing points for user " + user + ": User not found in DB")
return err
}
filter := bson.D{primitive.E{Key: "username", Value: user}}
opts := options.Update().SetUpsert(true)
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "points", Value: v.Points - p}}}}
_, err = userCollection.UpdateOne(context.TODO(), filter, update, opts)
if err != nil {
log.Println("* Error updating points in ")
}
return nil
}
func DBAddPoints(user string, p int) error {
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
var v ExistingAccount
err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v)
if err != nil {
fmt.Println("* Error editing points for user " + user + ": User not found in DB")
return err
}
filter := bson.D{primitive.E{Key: "username", Value: user}}
opts := options.Update().SetUpsert(true)
update := bson.D{primitive.E{Key: "$set", Value: bson.D{primitive.E{Key: "points", Value: v.Points + p}}}}
_, err = userCollection.UpdateOne(context.TODO(), filter, update, opts)
if err != nil {
log.Println("* Error updating points in ")
}
return nil
}
func DBGetUserByUsername(user string) ExistingAccount {
err := DB.Ping(context.Background(), readpref.Primary())
if err != nil {
DB = openDB()
}
userCollection := DB.Database("Users").Collection("Users")
var v ExistingAccount
err = userCollection.FindOne(context.Background(), bson.D{primitive.E{Key: "username", Value: user}}).Decode(&v)
if err != nil {
fmt.Println("* Error getting user " + user + ": Not found in DB")
var e ExistingAccount
return e
}
return v
}

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,24 @@
# massflip
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,46 @@
{
"name": "massflip",
"version": "0.0.2",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"dotenv": "^16.0.1",
"pinia": "^2.0.0-rc.10",
"vue": "^3.2.13",
"vue-gtag": "^2.0.1"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Massflip</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Concert+One&family=Fredoka+One&display=swap" rel="stylesheet">
</head>
<body style="background-color: #000814;">
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1,71 @@
<template>
<div id="Page">
<MassFlip />
<PageFooter />
</div>
</template>
<script setup>
import MassFlip from './components/MassFlip.vue'
import PageFooter from './components/PageFooter.vue'
import { userStore } from './store.js'
import { WSSend } from './main.js'
import { onMounted } from 'vue'
onMounted(() => {
let cookieStr = document.cookie
function cookiesToObj(str) {
str = str.split('; ');
var result = {};
for (var i = 0; i < str.length; i++) {
var cur = str[i].split('=');
result[cur[0]] = cur[1];
}
return result;
}
let jar = cookiesToObj(cookieStr)
if ("session" in jar) {
let id = jar["session"]
// handle logged in user
let req = new XMLHttpRequest
req.open("POST", "/api/login/bysession")
req.send(JSON.stringify({
session: id
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
let usr = JSON.parse(req.responseText)
if ("error" in usr) {
document.cookie = "session=; Max-Age=-99999999"
console.log(usr["error"])
return
}
userStore().updateUser(usr)
let msg = JSON.stringify({
type: "bind",
username: usr.username
})
WSSend(msg)
}
}
}
})
</script>
<style>
#Page {
height: 100%;
width: 100%;
}
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
display: flex;
flex-direction: column;
}
body {
background-color: #000814;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

@ -0,0 +1,58 @@
<template>
<div id="AboutBG">
<div id="close" @click="$emit('display', 'game')">X</div>
<div id="About">
<p>
<span class="logo">Massflip</span> was created by MNRVA as a way to learn about frontend
frameworks, websockets, APIs, and databases. The goal for <span class="logo">Massflip</span>
is to eventually become an open source coin-flip-game web server, with integration in twitch
streams, discord bots, etc., and also to be a project to put on my resume :)
<br>
<br>
If you enjoy this site, share it with your friends! This game thrives on the community chatting
and betting against each other, so more players are always welcome.
</p>
</div>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
defineEmits(['display'])
</script>
<style scoped>
#close {
color: #ccccd1;
font-size: 26px;
position: absolute;
top: 30px;
right: 50px;
font-family: 'Fredoka One';
}
#close:hover {
cursor: pointer;
color: #888;
}
#About {
width: 40%;
padding: 30px;
margin: auto;
margin-top: 5em;
border-radius: 30px;
background-color: rgba(0, 30, 61, 1);
}
p {
font-family: 'Concert One', cursive;
font-size: 18px;
color: #f3f9f8;
}
.logo {
font-family: 'Fredoka One', cursive;
font-size: 20px;
background-image: linear-gradient(0deg, #efbe08, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
</style>

@ -0,0 +1,254 @@
<template>
<div id="GameWindow">
<div id="Timer">
<div id="timeUntilFlip">{{ clock }}</div>
<div id="until">{{ until }}</div>
</div>
<div id="BetGraph">
<div class="pie animate pie-base" :style="tailsStyle"></div>
<div class="pie animate start-no-round pie-overlap" :style="headsStyle"></div>
<div class="data-overlap" id="data">
<div id="headsInfo" class="percent heads">{{ headsPercent }}%</div>
<div id="headsPool" class="pool heads"> {{ headsPool }}gp</div>
<div id="tailsInfo" class="percent tails">{{ tailsPercent }}%</div>
<div id="tailsPool" class="pool tails">{{ tailsPool }}gp</div>
</div>
</div>
<div id="lower" v-show="userStore().username != ''">
<BetInput />
<div id="gpCount">
Your GP:
<div id="gp">
{{ userStore().points }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import BetInput from './BetInput.vue'
import { WS } from '../main.js'
import { onMounted,ref,reactive } from 'vue'
const User = userStore()
const clock = ref('0:00s')
const until = ref('until next flip')
const headsPercent = ref(0)
const tailsPercent = ref(0)
const headsPool = ref(0)
const tailsPool = ref(0)
const headsStyle = reactive({
"--p":50,
"--c":"#efbe08", //unfocused #7a661b
"--b":"30px"
})
const tailsStyle = reactive({
"--p":100,
"--c":"#dc2f91", //unfocused #6b214b
"--b":"30px"
})
User.$subscribe(() => {
if (User.bet == 'heads') {
tailsStyle['--c'] = '#6b214b'
} else if (User.bet == 'tails') {
headsStyle['--c'] = '#7a661b'
} else {
headsStyle['--c'] = '#efbe08'
tailsStyle['--c'] = '#dc2f91'
}
})
onMounted(() => {
function updatePool(wsMsg) {
let hP = Math.floor((wsMsg.headspool / (wsMsg.headspool + wsMsg.tailspool)) * 100)
if (isNaN(hP)) {
headsPercent.value = 0
headsStyle['--p'] = 50
} else {
headsPercent.value = hP
headsStyle['--p'] = headsPercent.value
}
if (isNaN(hP) || wsMsg.tailspool == 0) {
tailsPercent.value = 0
} else {
tailsPercent.value = (100 - hP)
}
headsPool.value = wsMsg.headspool
tailsPool.value = wsMsg.tailspool
}
WS.addEventListener("message", function (evt) {
let wsMsg = JSON.parse(evt.data)
if (wsMsg.type == "pool") {
updatePool(wsMsg)
} else if (wsMsg.type == "win") {
userStore().addPoints(wsMsg.value)
} else if (wsMsg.type == "tick") {
let time = wsMsg.clock
let timeString = (Math.floor(time/60)).toString() + ":" + ((time%60)>9?"":"0") + (time%60).toString() + "s"
clock.value = timeString
until.value = 'until next flip'
updatePool(wsMsg)
} else if (wsMsg.type == "flip") {
clock.value = wsMsg.value
until.value = ''
userStore().setBet("")
} else if (wsMsg.type == "hasbet") {
if (wsMsg.value == true) {
userStore().bet = wsMsg.bet
}
}
})
})
</script>
<style scoped>
#GameWindow {
display: flex;
align-items: center;
flex-direction: column;
margin: auto;
justify-content: space-around;
height: 90%;
}
#BetGraph {
height: 350px;
width: 350px;
background-color: #000814;
border-radius: 175px;
user-select: none;
margin: none;
padding: 0;
}
#lower {
height: 200px;
}
@property --p{
syntax: '<number>';
inherits: true;
initial-value: 0;
}
.pie {
--p:50; /* the percentage */
--b:30px; /* the thickness */
--c:#444; /* the color */
--w:350px; /* the size*/
width:var(--w);
aspect-ratio:1/1;
position:fixed;
display:inline-grid;
place-content:center;
font-size:25px;
font-weight:bold;
font-family:sans-serif;
margin: 0px;
transition: all, .5s;
}
.pie:before,
.pie:after {
content:"";
position:absolute;
border-radius:50%;
}
.pie:before {
inset:0;
background:
radial-gradient(farthest-side,var(--c) 98%,#0000) top/var(--b) var(--b) no-repeat,
conic-gradient(var(--c) calc(var(--p)*1%),#0000 0);
-webkit-mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
}
.pie:after {
inset:calc(50% - var(--b)/2);
background:var(--c);
transform:rotate(calc(var(--p)*3.6deg - 90deg)) translate(calc(var(--w)/2 - 50%));
}
.animate {
animation:p 1s .5s both;
}
.start-no-round:before {
inset:0;
background:
radial-gradient(farthest-side,var(--c) 98%,#0000) top/var(--b) var(--b) no-repeat,
conic-gradient(var(--c) calc(var(--p)*1%),#0000 0);
-webkit-mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
mask:radial-gradient(farthest-side,#0000 calc(99% - var(--b)),#000 calc(100% - var(--b)));
}
.start-no-round:after {
inset:calc(50% - var(--b)/2);
background:var(--c);
transform:rotate(calc(var(--p)*3.6deg - 90deg)) translate(calc(var(--w)/2 - 50%));
}
@keyframes p{
from{--p:0}
}
.pie-base {
position: absolute;
}
.pie-overlap {
position:relative;
}
.data-overlap {
position:relative;
bottom:305px;
left: 50px;
background-color: #001d3d;
width: 250px;
height: 250px;
border-radius: 125px;
}
#data-bg {
height: 150px;
background-color: #001d3d;
}
.percent {
font-family: 'Fredoka One', cursive;
font-size: 48px;
margin: 0px;
}
.pool {
font-family: 'Fredoka One', cursive;
font-size: 15px;
margin-top: 5px;
}
.heads {
color:#efbe08;
}
.tails {
color:#dc2f91;
}
.pool.heads {
padding-bottom: 2em;
}
.heads.percent {
padding-top: .5em;
}
#Timer {
font-family: 'Fredoka One', cursive;
font-size: 56px;
}
#until {
font-size: 18px;
padding-bottom: 2em;
height: 20px;
}
#gpCount {
font-family: 'Concert One', cursive;
font-size: 24px;
}
#gp {
font-size: 36px;
background-image: linear-gradient(0deg, #efbe08, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
</style>

@ -0,0 +1,108 @@
<template>
<div id="BetInput">
<input id="betAmount" :class="{ badInput: hasError}" :disabled="userStore().bet != ''" v-model="bet" type="number" placeholder="Place a bet:"/>
<div id="buttons">
<button id="betHeads" :class="{ disable: userStore().bet != ''}" @click="submitBet('heads')" type="button">Heads</button>
<button id="betTails" :class="{ disable: userStore().bet != ''}" @click="submitBet('tails')" type="button">Tails</button>
</div>
<div id="err"> {{ error }}</div>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import { WSSend } from '../main.js'
import { ref } from 'vue'
const bet = ref()
const hasError = ref(false)
const error = ref('')
function submitBet(HorT){
if (userStore().bet != '') {
return
}
if (bet.value <= 0) {
hasError.value = true
error.value = "Error: bet must be greater than 0"
return
}
if (userStore().points - bet.value < 0) {
hasError.value = true
error.value = "Error: you cannot bet more gp than you have"
return
}
error.value = ""
hasError.value = false
let wsMsg = {
type: "bet",
username: userStore().username.toLowerCase(),
bet: HorT,
amount: bet.value,
}
WSSend(JSON.stringify(wsMsg));
userStore().subtractPoints(wsMsg.amount)
bet.value = ""
userStore().setBet(HorT)
}
</script>
<style scoped>
#BetInput {
display: flex;
align-items: center;
flex-direction: column;
padding-top: 2em;
max-width: 150px;
}
input {
height: 25px;
width: 100%;
border-radius: 30px;
border: 1px solid white;
color:#000814;
font-size: 12px;
font-weight: bold;
padding-left: 10px;
}
input:focus {
outline: none;
}
#buttons {
display: flex;
justify-content: space-around;
gap: 10px;
}
button {
background: none;
border: none;
color: white;
padding: 10px;
cursor: pointer;
font-family: 'Concert One', cursive;
font-size: 22px;
}
#betHeads {
color: #efbe08;
}
#betTails {
color: #dc2f91;
}
#betHeads:hover {
color: #7a661b;
}
#betTails:hover {
color:#6b214b;
}
#err {
color:red;
}
#betHeads.disable {
color: #7a661b;
cursor: default;
}
#betTails.disable {
color:#6b214b;
cursor: default;
}
</style>

@ -0,0 +1,163 @@
<template>
<div id="ChatBoxContainer">
<div id="ChatWindow">
</div>
<div id="ChatInputContainer">
<div v-if="userStore().username != ''">
<iframe name="cca" style="display:none;"></iframe>
<form action="#" target="cca" autocomplete="off">
<input id="ChatInput" v-model="chat" type="text" :placeholder="ChatString" maxlength="336"/>
<button id="submit" @click="sendChat">Send</button>
</form>
</div>
<p v-show="userStore().username == ''">You need to be logged in to chat</p>
</div>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import { Queue } from '../main.js'
import { WS,WSSend } from '../main.js'
import { ref,onMounted, computed } from 'vue'
const chatQueue = new Queue()
const ChatString = computed(() => {
return `Chat as ${userStore().username}:`
})
const chat = ref('')
const ChatColors = {
green: "limegreen",
yellow: "gold",
cyan: "cyan",
red: "firebrick",
pink: "fuchsia",
violet: "violet",
orange: "orange",
}
const _CHAT_MAX_HISTORY = 75;
onMounted(() => {
let log = document.getElementById("ChatWindow")
function appendLog(item) {
var doScroll = log.scrollTop > log.scrollHeight - log.clientHeight - 1;
log.appendChild(item);
if (doScroll) {
log.scrollTop = log.scrollHeight - log.clientHeight;
}
}
WS.addEventListener("message", function (evt) {
let wsMsg = JSON.parse(evt.data)
if (wsMsg.type == "chat") {
chatQueue.enqueue(wsMsg)
if (chatQueue.length >= _CHAT_MAX_HISTORY) {
chatQueue.dequeue()
}
log.innerHTML = ""
for (let message of Object.values(chatQueue.elements)) {
var item = document.createElement("div")
let fromUser = document.createElement("span")
fromUser.style = `color: ${message.color};`
fromUser.innerText = message.username
item.appendChild(fromUser)
let chatScore = document.createElement("span")
chatScore.innerText = `(${message.points})`
chatScore.style = `color: ${message.color};font-family: 'Helvetica';font-size: 12px;`
item.appendChild(chatScore)
let chatMsg = document.createTextNode(`: ${message.message}`)
item.appendChild(chatMsg)
appendLog(item)
}
}
})
})
function sendChat() {
if (chat.value.startsWith("/color")) {
let newColor = chat.value.split(" ")[1]
if (newColor in ChatColors) {
userStore().$patch({color:ChatColors[newColor]})
chat.value = ''
let req = new XMLHttpRequest()
req.open("PUT", "/api/chatcolor")
req.send(JSON.stringify({
username: userStore().username,
color: ChatColors[newColor],
}))
return
} else if (newColor == 'list') {
// put a chat message that shows all the colors
chat.value = ''
return
}
}
let wsSend = {
type: "chat",
username: userStore().username,
color: userStore().color,
message: chat.value,
points: userStore().points
}
WSSend(JSON.stringify(wsSend))
chat.value = ''
}
</script>
<style scoped>
#ChatBoxContainer {
height: 70vh;
width: 420px;
max-width: 50%;
margin: 2vh;
border-radius: 30px;
background-color: #000814;
text-align: left;
font-family: 'Concert One', cursive;
}
#ChatInputContainer p {
text-align: center;
}
#ChatWindow {
box-sizing: border-box;
height: 92%;
padding: 20px;
margin: 1vh;
margin-bottom: 5px;
overflow-y: scroll;
overflow-x: hidden;
scrollbar-color: #001d3d;
scrollbar-width: thin;
font-size: 16px;
}
#ChatWindow::-webkit-scrollbar {
background-color: #000814;
}
#ChatInput {
height: 20px;
width: 75%;
margin-left: 15px;
border-radius: 30px;
border: 1px solid white;
color:#000814;
font-size: 12px;
font-weight: bold;
padding-left: 10px;
}
#submit {
background-color: #000814;
color: #f3f9f8;
margin-left: 10px;
outline: none;
border: none;
font-weight: bold;
}
#submit:hover {
cursor: pointer;
color:grey;
}
#ChatInput:focus {
outline: none !important;
}
</style>

@ -0,0 +1,18 @@
<template>
<div id="GameWindow">
<BetBox />
<ChatBox />
</div>
</template>
<script setup>
import BetBox from './BetBox.vue'
import ChatBox from './ChatBox.vue'
</script>
<style scoped>
#GameWindow {
display: flex;
justify-content: space-around;
}
</style>

@ -0,0 +1,60 @@
<template>
<div id="HowToBG">
<div id="close" @click="$emit('display', 'game')">X</div>
<div id="HowTo">
<p>
The idea of <span class="logo">Massflip</span> is simple: place a bet on a coinflip.
Every player starts with the same amount of GP - the game's currency. The goal is to win
as much GP as possible without going bankrupt.
<br>
The game's clock will tick
down to each coin flip, and in that time you can enter a bet into the Betting input,
and click on either heads or tails to place your bet. If you win, you get a payout based on
how much GP was bet against you, and how much you contributed to the winning side's GP.
<br>
Compete with your friends to see how much GP you can win without going bankrupt!
</p>
</div>
</div>
</template>
<script setup>
import { defineEmits } from 'vue'
defineEmits(['display'])
</script>
<style scoped>
#close {
color: #ccccd1;
font-size: 26px;
position: absolute;
top: 30px;
right: 50px;
font-family: 'Fredoka One';
}
#close:hover {
cursor: pointer;
color: #888;
}
#HowTo {
width: 40%;
padding: 30px;
margin: auto;
margin-top: 5em;
border-radius: 30px;
background-color: rgba(0, 30, 61, 1);
}
p {
font-family: 'Concert One', cursive;
font-size: 18px;
color: #f3f9f8;
}
.logo {
font-family: 'Fredoka One', cursive;
font-size: 20px;
background-image: linear-gradient(0deg, #efbe08, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
</style>

@ -0,0 +1,201 @@
<template>
<div id="LoginBackground">
<div id="close" @click="$emit('display', 'game')">X</div>
<div id="LoginContainer">
<form action="none">
<label>Username</label>
<input type="text" :class="{ badInput: unHasError }" v-model="form.username" required="yes" maxlength="24" />
<label>Password</label>
<input type="password" :class="{ badInput: pHasError }" v-model="form.password" required="yes" maxlength="255">
<label v-show="tCreate">Confirm Password</label>
<input type="password" :class="{ badInput: pHasError }" v-model="form.confirm" required="yes" id="confPass" v-show="tCreate">
<div id="midRow">
<div>
<input type="checkbox" v-model="form.remember" id="remember">
<label id="rememberLabel">Remember me</label>
</div>
<div id="noAcc" @click="toggleCreate">Don't have an account?</div>
</div>
<button class="submit" v-show="!tCreate" @click="login">Login</button>
<button class="submit" v-show="tCreate" @click="createAccount">Create</button>
</form>
<p id="serverResponse"> {{ serverResponse }}</p>
</div>
</div>
</template>
<script setup>
import { defineEmits, ref, reactive } from 'vue'
defineEmits(['display'])
const form = reactive({
username: '',
password: '',
confirm: '',
remember: false
})
const tCreate = ref(false)
const unHasError = ref(false)
const pHasError = ref(false)
const serverResponse = ref('')
function toggleCreate() {
tCreate.value = !tCreate.value
}
function loginFieldsReady() {
let ret = true
let UnameReg = /^[a-zA-Z0-9-_]{3,24}$/
if (form.username.search(UnameReg) === -1) {
unHasError.value = true
serverResponse.value = "Username must be 3-24 characters, and only contain letters, numbers, - and _"
ret = false
} else {
unHasError.value = false
}
if (form.password.length < 8) {
pHasError.value = true
serverResponse.value = "Password must be at least 8 characters"
ret = false
} else if (form.password.length > 255){
pHasError.value = true
serverResponse.value = "Password must be no larger than 255 characters"
ret = false
} else {
pHasError.value = false
}
return ret
}
async function createAccount(e) {
e.preventDefault()
if (!loginFieldsReady()) {
return
}
if (form.password != form.confirm) {
pHasError.value = true
serverResponse.value = "passwords do not match"
return
}
let req = new XMLHttpRequest()
req.open("POST", "/api/createaccount")
req.withCredentials = true
req.send(JSON.stringify({
username: form.username,
password: form.password,
remember: form.remember
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
let resp = JSON.parse(req.response)
let err = resp["error"]
if (err == undefined) {
location.reload()
} else {
document.getElementById("serverResponse").innerText = `Error: ${err}`;
}
}
}
}
function login(e) {
e.preventDefault()
if (!loginFieldsReady()) {
return
}
let req = new XMLHttpRequest()
req.open("POST", "/api/login")
req.withCredentials = true
req.send(JSON.stringify({
username: form.username,
password: form.password,
remember: form.remember
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
let resp = JSON.parse(req.response)
let err = resp["error"]
if (err == undefined) {
location.reload()
} else {
serverResponse.value = `Error: ${err}`;
}
}
}
}
</script>
<style scoped>
#close {
color: #ccccd1;
font-size: 26px;
position: absolute;
top: 30px;
right: 50px;
font-family: 'Fredoka One';
}
#close:hover {
cursor: pointer;
color: #888;
}
#LoginContainer {
width: 40%;
padding: 30px;
margin: auto;
margin-top: 5em;
border-radius: 30px;
background-color: rgba(0, 30, 61, 1);
}
#noAcc {
font-family: 'Concert One', cursive;
color:#417fc2;
font-size: 14px;
}
#noAcc:hover {
cursor: pointer;
color:#2b4d72;
}
#rememberLabel {
padding-left: 3px;
}
#midRow {
width: 100%;
display: flex;
justify-content: space-around;
align-items:flex-end;
}
form {
display: flex;
flex-flow: column;
align-items: flex-start;
}
form label {
font-family: 'Concert One', cursive;
padding-left: 1em;
}
input[type=text], input[type=password] {
width: 100%;
padding: 6px;
margin: 8px 0;
border-radius: 15px;
font-family: 'Concert One', cursive;
font-size: 16px;
}
.submit {
margin-top: 1em;
border: none;
width: 30%;
background: none;
color: #f3f9f8;
font-family: 'Fredoka One', cursive;
font-size: 24px;
align-self: flex-end;
}
.submit:hover {
cursor: pointer;
color: #979b9a;
}
#serverResponse {
font-family: 'Times New Roman', Times, serif;
color: rgb(212, 97, 117);
}
.badInput {
border: 2px solid red;
}
</style>

@ -0,0 +1,53 @@
<template>
<div id="Content">
<PageHeader @display="displayHandle"/>
<div id="Container">
<GameWindow />
<LoginForm @display="displayHandle" class="dullBG" v-show="display.content == 'login'"/>
<AboutPage @display="displayHandle" class="dullBG" v-show="display.content == 'about'"/>
<HowToPlay @display="displayHandle" class="dullBG" v-show="display.content == 'howTo'"/>
</div>
</div>
</template>
<script setup>
import PageHeader from './PageHeader.vue'
import LoginForm from './LoginForm.vue'
import GameWindow from './GameWindow.vue'
import AboutPage from './AboutPage.vue'
import HowToPlay from './HowToPlay.vue'
import { reactive } from 'vue'
const display = reactive({ content: 'game' })
function displayHandle(c) {
if (display.content == c) {
display.content = 'game'
} else {
display.content = c
}
}
</script>
<style scoped>
#Container {
width: 1000px;
max-width: 100%;
height: 75vh;
border-radius: 30px;
margin: auto;
color: #f3f9f8;
background-color: #001d3d;
position: relative;
}
.dullBG {
height: 100%;
width: 100%;
position: absolute;
top: 0px;
left: 0px;
border-radius: 30px;
background-color: rgba(0, 8, 20,0.7);
}
</style>

@ -0,0 +1,13 @@
<template>
<p>Massflip v0.0.3 - Copyright 2022 MNRVA</p>
</template>
<script setup>
</script>
<style scoped>
p {
font-family: 'Concert One', cursive;
color: #001d3d;
}
</style>

@ -0,0 +1,67 @@
<template>
<div id="HeaderContainer">
<div id="header">Massflip<span id="beta">alpha</span></div>
<nav>
<p @click="$emit('display', 'howTo')">How to play</p>
<p @click="$emit('display', 'about')">About</p>
<p v-if="userStore().username == ''" @click="$emit('display', 'login')" id="showLoginNav">Login</p>
<p v-if="userStore().username != ''" @click="logout()">Logout</p>
</nav>
</div>
</template>
<script setup>
import { userStore } from '../store.js'
import { defineEmits } from 'vue'
defineEmits(['display'])
function logout() {
let req = new XMLHttpRequest()
req.open("POST", "/api/logout")
req.send(JSON.stringify({
username: userStore().username
}))
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE) {
document.cookie = "session=; Max-Age=-99999999"
location.reload()
}
}
}
</script>
<style scoped>
#HeaderContainer {
color: #f3f9f8;
display: flex;
height: 125px;
flex-direction: column;
align-items: center;
user-select: none;
font-family: 'Fredoka One', cursive;
font-size: 48px;
margin: 0;
margin-top: 10px;
}
#header {
display: flex;
}
#beta {
font-size: 24px;
background-image: linear-gradient(0deg, #efbe08 60%, #dc2f91);
background-size: 100%;
background-clip: text;
color: transparent;
}
nav {
display: flex;
align-items: center;
font-family: 'Concert One', cursive;
width: 400px;
justify-content: space-between;
font-size: 18px;
}
nav p:hover {
cursor: pointer;
color: #bbb;
}
</style>

@ -0,0 +1,69 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import VueGtag from "vue-gtag";
// switch these in production
export const WS = new WebSocket("wss://" + "massflip.mnrva.dev" + "/ws")
//export const WS = new WebSocket("ws://" + "127.0.0.1:8000" + "/ws")
WS.onclose = function() {
alert("WebSocket connection closed.")
}
WS.onerror = function() {
alert("WebSocket connection error.")
}
export function WSSend(msg){
WSWait(WS, function(){
WS.send(msg);
});
}
function WSWait(socket, callback){
setTimeout(
function () {
if (socket.readyState === WebSocket.OPEN) {
if (callback != null){
callback();
}
} else {
WSWait(socket, callback);
}
}, 5);
}
export class Queue {
constructor() {
this.elements = {};
this.head = 0;
this.tail = 0;
}
enqueue(element) {
this.elements[this.tail] = element;
this.tail++;
}
dequeue() {
const item = this.elements[this.head];
delete this.elements[this.head];
this.head++;
return item;
}
peek() {
return this.elements[this.head];
}
get length() {
return this.tail - this.head;
}
get isEmpty() {
return this.length === 0;
}
}
const pinia = createPinia()
var app = createApp(App)
app.use(pinia)
app.use(VueGtag, {config: { id: "G-C3WQH98SZB" }})
app.mount('#app')

@ -0,0 +1,61 @@
import { defineStore } from 'pinia'
// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const userStore = defineStore({
id:'main',
state: () => {
return {
username: '',
color: '',
points: 0,
resets: 0,
bet: "",
}
},
getters: {
getUser() {
return {
username: this.username,
color: this.color,
points: this.points,
resets: this.resets,
}
},
ready() {
if (this.username != '') {
return true
} else {
return false
}
},
getBet() {
return this.bet
},
getUsername() {
return this.username
}
},
actions: {
updateUser(usr) {
this.$state = {
username: usr.username,
color: usr.color,
points: usr.points,
resets: usr.resets,
}
},
addPoints(amt) {
this.$patch({points: this.points + amt})
},
subtractPoints(amt) {
this.$patch({points: this.points - amt})
},
toggle() {
this.$patch({t:!this.t})
},
setBet(bet) {
this.$patch({bet: bet})
}
}
})

@ -0,0 +1,17 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
module.exports = {
devServer: {
host: "localhost",
proxy: {
"/": {
target: "http://localhost:8000",
secure: false,
ws: true
}
}
}
};

@ -0,0 +1,25 @@
module github.com/mnrva-dev/massflip
go 1.18
require github.com/go-chi/chi/v5 v5.0.7 // direct
require github.com/gorilla/websocket v1.5.0 // direct
require (
github.com/go-chi/cors v1.2.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.0.2 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.mongodb.org/mongo-driver v1.8.4 // indirect
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/text v0.3.5 // indirect
)

@ -0,0 +1,59 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.0 h1:tV1g1XENQ8ku4Bq3K9ub2AtgG+p16SmzeMSGTwrOKdE=
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
go.mongodb.org/mongo-driver v1.8.4 h1:NruvZPPL0PBcRJKmbswoWSrmHeUvzdxA3GCPfD/NEOA=
go.mongodb.org/mongo-driver v1.8.4/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU=
golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -0,0 +1,47 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"github.com/go-chi/chi/v5"
)
/*
* TODO:
Later:
- user pages
- figure out an actual goal for the game
*
*/
func main() {
// prepare router
r := chi.NewRouter()
// open hub and start game
chatHub := newHub()
go chatHub.run()
go chatHub.runGameClock()
// disconnect to DB on application exit
defer DB.Disconnect(context.Background())
// handlers
r.Handle("/*", http.FileServer(http.Dir("./frontend/dist")))
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(chatHub, w, r)
})
r.Post("/api/createaccount", createAccount)
r.Post("/api/login", login)
r.Post("/api/login/bysession", loginBySession)
r.Post("/api/logout", logout)
r.Put("/api/chatcolor", chatColor)
// run server
fmt.Println("* Listening on localhost:8000")
log.Fatal(http.ListenAndServe(":8000", r))
}
Loading…
Cancel
Save