mirror of https://github.com/dirtbags/moth.git
Merge branch 'main' of https://github.com/dirtbags/moth into main
This commit is contained in:
commit
c54a6dbb1a
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -4,7 +4,14 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [v4.0.0] - Unreleased
|
## [v4.0.0-rc2] - Unreleased
|
||||||
|
### Fixed
|
||||||
|
- Multiple bugs preventing production server from working properly
|
||||||
|
- CI builds should be working now
|
||||||
|
- Team registration now correctly writes names to files
|
||||||
|
- Anonymized team names now only computed once per team
|
||||||
|
|
||||||
|
## [v4.0-rc1] - 2020-10-13
|
||||||
### Changed
|
### Changed
|
||||||
- Major rewrite/refactor of `mothd`
|
- Major rewrite/refactor of `mothd`
|
||||||
- Clear separation of roles: State, Puzzles, and Theme
|
- Clear separation of roles: State, Puzzles, and Theme
|
||||||
|
@ -37,14 +44,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
## [Unreleased]
|
|
||||||
### Changed
|
|
||||||
- Endpoints `/points.json`, `/puzzles.json`, and `/messages.html` (optional theme file) combine into `/state`
|
|
||||||
- No more `__devel__` category for dev server: this is now `.config.devel` in the `/state` endpoint
|
|
||||||
- Development server no longer serves a static `/` with links: it now redirects you to a randomly-generated seed URL
|
|
||||||
- Default theme modifications to handle all this
|
|
||||||
- Default theme now automatically "logs you in" with Team ID if it's getting state from the devel server
|
|
||||||
|
|
||||||
## [v3.5.1] - 2020-03-16
|
## [v3.5.1] - 2020-03-16
|
||||||
### Fixed
|
### Fixed
|
||||||
- Support insta-checking for legacy puzzles
|
- Support insta-checking for legacy puzzles
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -109,7 +110,15 @@ func (h *HTTPServer) StateHandler(mh MothRequestHandler, w http.ResponseWriter,
|
||||||
// RegisterHandler handles attempts to register a team
|
// RegisterHandler handles attempts to register a team
|
||||||
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
func (h *HTTPServer) RegisterHandler(mh MothRequestHandler, w http.ResponseWriter, req *http.Request) {
|
||||||
teamName := req.FormValue("name")
|
teamName := req.FormValue("name")
|
||||||
if err := mh.Register(teamName); err != nil {
|
teamName = strings.TrimSpace(teamName)
|
||||||
|
if teamName == "" {
|
||||||
|
jsend.Sendf(w, jsend.Fail, "empty name", "Team name may not be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mh.Register(teamName); err == ErrAlreadyRegistered {
|
||||||
|
jsend.Sendf(w, jsend.Success, "already registered", "Team ID has already been registered")
|
||||||
|
} else if err != nil {
|
||||||
jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
|
jsend.Sendf(w, jsend.Fail, "not registered", err.Error())
|
||||||
} else {
|
} else {
|
||||||
jsend.Sendf(w, jsend.Success, "registered", "Team ID registered")
|
jsend.Sendf(w, jsend.Success, "registered", "Team ID registered")
|
||||||
|
@ -171,11 +180,12 @@ func (h *HTTPServer) MothballerHandler(mh MothRequestHandler, w http.ResponseWri
|
||||||
// parts[0] == "mothballer"
|
// parts[0] == "mothballer"
|
||||||
filename := parts[1]
|
filename := parts[1]
|
||||||
cat := strings.TrimSuffix(filename, ".mb")
|
cat := strings.TrimSuffix(filename, ".mb")
|
||||||
mothball, err := mh.Mothball(cat)
|
mb := new(bytes.Buffer)
|
||||||
if err != nil {
|
if err := mh.Mothball(cat, mb); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.ServeContent(w, req, filename, time.Now(), mothball)
|
mbReader := bytes.NewReader(mb.Bytes())
|
||||||
|
http.ServeContent(w, req, filename, time.Now(), mbReader)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,8 +50,8 @@ func TestHttpd(t *testing.T) {
|
||||||
|
|
||||||
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
||||||
t.Error(r.Result())
|
t.Error(r.Result())
|
||||||
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":""},"PointsLog":[],"Puzzles":{"pategory":[1]}}` {
|
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{},"PointsLog":[],"Puzzles":{}}` {
|
||||||
t.Error("Unexpected state")
|
t.Error("Unexpected state", r.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 {
|
if r := hs.TestRequest("/register", map[string]string{"id": "bad team id", "name": "GoTeam"}); r.Result().StatusCode != 200 {
|
||||||
|
@ -66,6 +66,12 @@ func TestHttpd(t *testing.T) {
|
||||||
t.Error("Register failed")
|
t.Error("Register failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r := hs.TestRequest("/register", map[string]string{"name": "GoTeam"}); r.Result().StatusCode != 200 {
|
||||||
|
t.Error(r.Result())
|
||||||
|
} else if r.Body.String() != `{"status":"success","data":{"short":"already registered","description":"Team ID has already been registered"}}` {
|
||||||
|
t.Error("Register failed", r.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
||||||
t.Error(r.Result())
|
t.Error(r.Result())
|
||||||
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` {
|
} else if r.Body.String() != `{"Config":{"Devel":false},"Messages":"messages.html","TeamNames":{"self":"GoTeam"},"PointsLog":[],"Puzzles":{"pategory":[1]}}` {
|
||||||
|
@ -108,6 +114,10 @@ func TestHttpd(t *testing.T) {
|
||||||
|
|
||||||
time.Sleep(TestMaintenanceInterval)
|
time.Sleep(TestMaintenanceInterval)
|
||||||
|
|
||||||
|
if r := hs.TestRequest("/content/pategory/2/puzzle.json", nil); r.Result().StatusCode != 200 {
|
||||||
|
t.Error(r.Result())
|
||||||
|
}
|
||||||
|
|
||||||
state := StateExport{}
|
state := StateExport{}
|
||||||
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
if r := hs.TestRequest("/state", nil); r.Result().StatusCode != 200 {
|
||||||
t.Error(r.Result())
|
t.Error(r.Result())
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -52,7 +51,7 @@ func (m *Mothballs) Open(cat string, points int, filename string) (ReadSeekClose
|
||||||
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
|
return nil, time.Time{}, fmt.Errorf("No such category: %s", cat)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := zc.Open(fmt.Sprintf("content/%d/%s", points, filename))
|
f, err := zc.Open(fmt.Sprintf("%d/%s", points, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, time.Time{}, err
|
return nil, time.Time{}, err
|
||||||
}
|
}
|
||||||
|
@ -174,8 +173,8 @@ func (m *Mothballs) refresh() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mothball just returns an error
|
// Mothball just returns an error
|
||||||
func (m *Mothballs) Mothball(cat string) (*bytes.Reader, error) {
|
func (m *Mothballs) Mothball(cat string, w io.Writer) error {
|
||||||
return nil, fmt.Errorf("Can't repackage a compiled mothball")
|
return fmt.Errorf("Refusing to repackage a compiled mothball")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maintain performs housekeeping for Mothballs.
|
// Maintain performs housekeeping for Mothballs.
|
||||||
|
|
|
@ -13,12 +13,12 @@ var testFiles = []struct {
|
||||||
}{
|
}{
|
||||||
{"puzzles.txt", "1\n3\n2\n"},
|
{"puzzles.txt", "1\n3\n2\n"},
|
||||||
{"answers.txt", "1 answer123\n1 answer456\n2 wat\n"},
|
{"answers.txt", "1 answer123\n1 answer456\n2 wat\n"},
|
||||||
{"content/1/puzzle.json", `{"name": "moo"}`},
|
{"1/puzzle.json", `{"name": "moo"}`},
|
||||||
{"content/1/moo.txt", `moo`},
|
{"1/moo.txt", `moo`},
|
||||||
{"content/2/puzzle.json", `{}`},
|
{"2/puzzle.json", `{}`},
|
||||||
{"content/2/moo.txt", `moo`},
|
{"2/moo.txt", `moo`},
|
||||||
{"content/3/puzzle.json", `{}`},
|
{"3/puzzle.json", `{}`},
|
||||||
{"content/3/moo.txt", `moo`},
|
{"3/moo.txt", `moo`},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mothballs) createMothball(cat string) {
|
func (m *Mothballs) createMothball(cat string) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -42,7 +41,7 @@ type PuzzleProvider interface {
|
||||||
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
Open(cat string, points int, path string) (ReadSeekCloser, time.Time, error)
|
||||||
Inventory() []Category
|
Inventory() []Category
|
||||||
CheckAnswer(cat string, points int, answer string) (bool, error)
|
CheckAnswer(cat string, points int, answer string) (bool, error)
|
||||||
Mothball(cat string) (*bytes.Reader, error)
|
Mothball(cat string, w io.Writer) error
|
||||||
Maintainer
|
Maintainer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +107,7 @@ type MothRequestHandler struct {
|
||||||
// PuzzlesOpen opens a file associated with a puzzle.
|
// PuzzlesOpen opens a file associated with a puzzle.
|
||||||
// BUG(neale): Multiple providers with the same category name are not detected or handled well.
|
// BUG(neale): Multiple providers with the same category name are not detected or handled well.
|
||||||
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (r ReadSeekCloser, ts time.Time, err error) {
|
func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (r ReadSeekCloser, ts time.Time, err error) {
|
||||||
export := mh.ExportState()
|
export := mh.exportStateIfRegistered(true)
|
||||||
found := false
|
found := false
|
||||||
for _, p := range export.Puzzles[cat] {
|
for _, p := range export.Puzzles[cat] {
|
||||||
if p == points {
|
if p == points {
|
||||||
|
@ -116,7 +115,7 @@ func (mh *MothRequestHandler) PuzzlesOpen(cat string, points int, path string) (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
return nil, time.Time{}, fmt.Errorf("Category not found")
|
return nil, time.Time{}, fmt.Errorf("Puzzle does not exist or is locked")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try every provider until someone doesn't return an error
|
// Try every provider until someone doesn't return an error
|
||||||
|
@ -173,27 +172,37 @@ func (mh *MothRequestHandler) Register(teamName string) error {
|
||||||
// the anonymized team name for this teamID has the special value "self".
|
// the anonymized team name for this teamID has the special value "self".
|
||||||
// If not, the puzzles list is empty.
|
// If not, the puzzles list is empty.
|
||||||
func (mh *MothRequestHandler) ExportState() *StateExport {
|
func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
|
return mh.exportStateIfRegistered(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mh *MothRequestHandler) exportStateIfRegistered(override bool) *StateExport {
|
||||||
export := StateExport{}
|
export := StateExport{}
|
||||||
export.Config = mh.Config
|
export.Config = mh.Config
|
||||||
|
|
||||||
teamName, _ := mh.State.TeamName(mh.teamID)
|
teamName, err := mh.State.TeamName(mh.teamID)
|
||||||
|
registered := override || (err == nil)
|
||||||
|
|
||||||
export.Messages = mh.State.Messages()
|
export.Messages = mh.State.Messages()
|
||||||
export.TeamNames = map[string]string{"self": teamName}
|
export.TeamNames = make(map[string]string)
|
||||||
|
|
||||||
// Anonymize team IDs in points log, and write out team names
|
// Anonymize team IDs in points log, and write out team names
|
||||||
pointsLog := mh.State.PointsLog()
|
pointsLog := mh.State.PointsLog()
|
||||||
exportIDs := map[string]string{mh.teamID: "self"}
|
exportIDs := make(map[string]string)
|
||||||
maxSolved := map[string]int{}
|
maxSolved := make(map[string]int)
|
||||||
export.PointsLog = make(award.List, len(pointsLog))
|
export.PointsLog = make(award.List, len(pointsLog))
|
||||||
|
|
||||||
|
if registered {
|
||||||
|
export.TeamNames["self"] = teamName
|
||||||
|
exportIDs[mh.teamID] = "self"
|
||||||
|
}
|
||||||
for logno, awd := range pointsLog {
|
for logno, awd := range pointsLog {
|
||||||
if id, ok := exportIDs[awd.TeamID]; ok {
|
if id, ok := exportIDs[awd.TeamID]; ok {
|
||||||
awd.TeamID = id
|
awd.TeamID = id
|
||||||
} else {
|
} else {
|
||||||
exportID := strconv.Itoa(logno)
|
exportID := strconv.Itoa(logno)
|
||||||
name, _ := mh.State.TeamName(awd.TeamID)
|
name, _ := mh.State.TeamName(awd.TeamID)
|
||||||
|
exportIDs[awd.TeamID] = exportID
|
||||||
awd.TeamID = exportID
|
awd.TeamID = exportID
|
||||||
exportIDs[awd.TeamID] = awd.TeamID
|
|
||||||
export.TeamNames[exportID] = name
|
export.TeamNames[exportID] = name
|
||||||
}
|
}
|
||||||
export.PointsLog[logno] = awd
|
export.PointsLog[logno] = awd
|
||||||
|
@ -205,11 +214,10 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
export.Puzzles = make(map[string][]int)
|
export.Puzzles = make(map[string][]int)
|
||||||
if _, ok := export.TeamNames["self"]; ok {
|
if registered {
|
||||||
// We used to hand this out to everyone,
|
// We used to hand this out to everyone,
|
||||||
// but then we got a bad reputation on some secretive blacklist,
|
// but then we got a bad reputation on some secretive blacklist,
|
||||||
// and now the Navy can't register for events.
|
// and now the Navy can't register for events.
|
||||||
|
|
||||||
for _, provider := range mh.PuzzleProviders {
|
for _, provider := range mh.PuzzleProviders {
|
||||||
for _, category := range provider.Inventory() {
|
for _, category := range provider.Inventory() {
|
||||||
// Append sentry (end of puzzles)
|
// Append sentry (end of puzzles)
|
||||||
|
@ -233,14 +241,16 @@ func (mh *MothRequestHandler) ExportState() *StateExport {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mothball generates a mothball for the given category.
|
// Mothball generates a mothball for the given category.
|
||||||
func (mh *MothRequestHandler) Mothball(cat string) (r *bytes.Reader, err error) {
|
func (mh *MothRequestHandler) Mothball(cat string, w io.Writer) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
if !mh.Config.Devel {
|
if !mh.Config.Devel {
|
||||||
return nil, fmt.Errorf("Cannot mothball in production mode")
|
return fmt.Errorf("Cannot mothball in production mode")
|
||||||
}
|
}
|
||||||
for _, provider := range mh.PuzzleProviders {
|
for _, provider := range mh.PuzzleProviders {
|
||||||
if r, err = provider.Mothball(cat); err == nil {
|
if err = provider.Mothball(cat, w); err == nil {
|
||||||
return r, nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,28 @@ func TestServer(t *testing.T) {
|
||||||
|
|
||||||
server := NewTestServer()
|
server := NewTestServer()
|
||||||
handler := server.NewHandler(participantID, teamID)
|
handler := server.NewHandler(participantID, teamID)
|
||||||
|
anonHandler := server.NewHandler("badParticipantId", "badTeamId")
|
||||||
|
|
||||||
|
{
|
||||||
|
es := handler.ExportState()
|
||||||
|
if es.Config.Devel {
|
||||||
|
t.Error("Marked as development server", es.Config)
|
||||||
|
}
|
||||||
|
if len(es.Puzzles) != 0 {
|
||||||
|
t.Log("State", es)
|
||||||
|
t.Error("Unauthenticated state has non-empty puzzles list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := handler.Register(teamName); err != nil {
|
if err := handler.Register(teamName); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
if err := handler.Register(teamName); err == nil {
|
||||||
|
t.Error("Registering twice should have raised an error")
|
||||||
|
} else if err != ErrAlreadyRegistered {
|
||||||
|
t.Error("Wrong error for duplicate registration:", err)
|
||||||
|
}
|
||||||
|
|
||||||
if r, _, err := handler.ThemeOpen("/index.html"); err != nil {
|
if r, _, err := handler.ThemeOpen("/index.html"); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if contents, err := ioutil.ReadAll(r); err != nil {
|
} else if contents, err := ioutil.ReadAll(r); err != nil {
|
||||||
|
@ -45,24 +64,26 @@ func TestServer(t *testing.T) {
|
||||||
t.Error("index.html wrong contents", contents)
|
t.Error("index.html wrong contents", contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
es := handler.ExportState()
|
{
|
||||||
if es.Config.Devel {
|
es := handler.ExportState()
|
||||||
t.Error("Marked as development server", es.Config)
|
if es.Config.Devel {
|
||||||
}
|
t.Error("Marked as development server", es.Config)
|
||||||
if len(es.Puzzles) != 1 {
|
}
|
||||||
t.Error("Puzzle categories wrong length")
|
if len(es.Puzzles) != 1 {
|
||||||
}
|
t.Error("Puzzle categories wrong length")
|
||||||
if es.Messages != "messages.html" {
|
}
|
||||||
t.Error("Messages has wrong contents")
|
if es.Messages != "messages.html" {
|
||||||
}
|
t.Error("Messages has wrong contents")
|
||||||
if len(es.PointsLog) != 0 {
|
}
|
||||||
t.Error("Points log not empty")
|
if len(es.PointsLog) != 0 {
|
||||||
}
|
t.Error("Points log not empty")
|
||||||
if len(es.TeamNames) != 1 {
|
}
|
||||||
t.Error("Wrong number of team names")
|
if len(es.TeamNames) != 1 {
|
||||||
}
|
t.Error("Wrong number of team names")
|
||||||
if es.TeamNames["self"] != teamName {
|
}
|
||||||
t.Error("TeamNames['self'] wrong")
|
if es.TeamNames["self"] != teamName {
|
||||||
|
t.Error("TeamNames['self'] wrong")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil {
|
if r, _, err := handler.PuzzlesOpen("pategory", 1, "moo.txt"); err != nil {
|
||||||
|
@ -77,12 +98,12 @@ func TestServer(t *testing.T) {
|
||||||
r.Close()
|
r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzles.json"); err == nil {
|
if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err == nil {
|
||||||
t.Error("Opening locked puzzle shouldn't work")
|
t.Error("Opening locked puzzle shouldn't work")
|
||||||
r.Close()
|
r.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzles.json"); err == nil {
|
if r, _, err := handler.PuzzlesOpen("pategory", 20, "puzzle.json"); err == nil {
|
||||||
t.Error("Opening non-existent puzzle shouldn't work")
|
t.Error("Opening non-existent puzzle shouldn't work")
|
||||||
r.Close()
|
r.Close()
|
||||||
}
|
}
|
||||||
|
@ -93,9 +114,53 @@ func TestServer(t *testing.T) {
|
||||||
|
|
||||||
time.Sleep(TestMaintenanceInterval)
|
time.Sleep(TestMaintenanceInterval)
|
||||||
|
|
||||||
es = handler.ExportState()
|
{
|
||||||
if len(es.PointsLog) != 1 {
|
es := handler.ExportState()
|
||||||
t.Error("I didn't get my points!")
|
if len(es.PointsLog) != 1 {
|
||||||
|
t.Error("I didn't get my points!")
|
||||||
|
}
|
||||||
|
if len(es.Puzzles["pategory"]) != 2 {
|
||||||
|
t.Error("The next puzzle didn't unlock!")
|
||||||
|
} else if es.Puzzles["pategory"][1] != 2 {
|
||||||
|
t.Error("The 2-point puzzle should have unlocked!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, _, err := handler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil {
|
||||||
|
t.Error("Opening unlocked puzzle should work")
|
||||||
|
} else {
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
if r, _, err := anonHandler.PuzzlesOpen("pategory", 2, "puzzle.json"); err != nil {
|
||||||
|
t.Error("Opening unlocked puzzle anonymously should work")
|
||||||
|
} else {
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler.CheckAnswer("pategory", 2, "wat"); err != nil {
|
||||||
|
t.Error("Right answer marked wrong:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(TestMaintenanceInterval)
|
||||||
|
|
||||||
|
{
|
||||||
|
es := anonHandler.ExportState()
|
||||||
|
if len(es.TeamNames) != 1 {
|
||||||
|
t.Error("Anonymous TeamNames is wrong:", es.TeamNames)
|
||||||
|
}
|
||||||
|
if len(es.PointsLog) != 2 {
|
||||||
|
t.Error("Points log wrong length")
|
||||||
|
}
|
||||||
|
if es.PointsLog[1].TeamID != "0" {
|
||||||
|
t.Error("Second point log didn't anonymize team ID correctly:", es.PointsLog[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
es := handler.ExportState()
|
||||||
|
if len(es.TeamNames) != 1 {
|
||||||
|
t.Error("TeamNames is wrong:", es.TeamNames)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG(neale): We aren't currently testing the various ways to disable the server
|
// BUG(neale): We aren't currently testing the various ways to disable the server
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
@ -23,6 +24,9 @@ const DistinguishableChars = "34678abcdefhikmnpqrtwxy="
|
||||||
// This is also a valid RFC3339 format.
|
// This is also a valid RFC3339 format.
|
||||||
const RFC3339Space = "2006-01-02 15:04:05Z07:00"
|
const RFC3339Space = "2006-01-02 15:04:05Z07:00"
|
||||||
|
|
||||||
|
// ErrAlreadyRegistered means a team cannot be registered because it was registered previously.
|
||||||
|
var ErrAlreadyRegistered = errors.New("Team ID has already been registered")
|
||||||
|
|
||||||
// State defines the current state of a MOTH instance.
|
// State defines the current state of a MOTH instance.
|
||||||
// We use the filesystem for synchronization between threads.
|
// We use the filesystem for synchronization between threads.
|
||||||
// The only thing State methods need to know is the path to the state directory.
|
// The only thing State methods need to know is the path to the state directory.
|
||||||
|
@ -127,7 +131,7 @@ func (s *State) TeamName(teamID string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTeamName writes out team name.
|
// SetTeamName writes out team name.
|
||||||
// This can only be done once.
|
// This can only be done once per team.
|
||||||
func (s *State) SetTeamName(teamID, teamName string) error {
|
func (s *State) SetTeamName(teamID, teamName string) error {
|
||||||
idsFile, err := s.Open("teamids.txt")
|
idsFile, err := s.Open("teamids.txt")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -147,14 +151,16 @@ func (s *State) SetTeamName(teamID, teamName string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
teamFilename := filepath.Join("teams", teamID)
|
teamFilename := filepath.Join("teams", teamID)
|
||||||
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_EXCL, 0644)
|
teamFile, err := s.Fs.OpenFile(teamFilename, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644)
|
||||||
if os.IsExist(err) {
|
if os.IsExist(err) {
|
||||||
return fmt.Errorf("Team ID is already registered")
|
return ErrAlreadyRegistered
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer teamFile.Close()
|
defer teamFile.Close()
|
||||||
|
log.Println("Setting team name to:", teamName, teamFilename, teamFile)
|
||||||
fmt.Fprintln(teamFile, teamName)
|
fmt.Fprintln(teamFile, teamName)
|
||||||
|
teamFile.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,12 +55,18 @@ func TestState(t *testing.T) {
|
||||||
t.Errorf("Setting bad team ID didn't raise an error")
|
t.Errorf("Setting bad team ID didn't raise an error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.SetTeamName(teamID, "My Team"); err != nil {
|
teamName := "My Team"
|
||||||
t.Errorf("Setting team name: %v", err)
|
if err := s.SetTeamName(teamID, teamName); err != nil {
|
||||||
|
t.Errorf("Setting team name: %w", err)
|
||||||
}
|
}
|
||||||
if err := s.SetTeamName(teamID, "wat"); err == nil {
|
if err := s.SetTeamName(teamID, "wat"); err == nil {
|
||||||
t.Errorf("Registering team a second time didn't fail")
|
t.Errorf("Registering team a second time didn't fail")
|
||||||
}
|
}
|
||||||
|
if name, err := s.TeamName(teamID); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
} else if name != teamName {
|
||||||
|
t.Error("Incorrect team name:", name)
|
||||||
|
}
|
||||||
|
|
||||||
category := "poot"
|
category := "poot"
|
||||||
points := 3928
|
points := 3928
|
||||||
|
@ -83,6 +89,10 @@ func TestState(t *testing.T) {
|
||||||
t.Error("Duplicate points award didn't fail")
|
t.Error("Duplicate points award didn't fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.AwardPoints(teamID, category, points+1); err != nil {
|
||||||
|
t.Error("Awarding more points:", err)
|
||||||
|
}
|
||||||
|
|
||||||
pl = s.PointsLog()
|
pl = s.PointsLog()
|
||||||
if len(pl) != 1 {
|
if len(pl) != 1 {
|
||||||
t.Errorf("After awarding points, points log has length %d", len(pl))
|
t.Errorf("After awarding points, points log has length %d", len(pl))
|
||||||
|
@ -98,7 +108,7 @@ func TestState(t *testing.T) {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
s.refresh()
|
s.refresh()
|
||||||
if len(s.PointsLog()) != 1 {
|
if len(s.PointsLog()) != 2 {
|
||||||
t.Error("Intentional parse error screws up all parsing")
|
t.Error("Intentional parse error screws up all parsing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,9 +70,9 @@ func (p TranspilerProvider) CheckAnswer(cat string, points int, answer string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mothball packages up a category into a mothball.
|
// Mothball packages up a category into a mothball.
|
||||||
func (p TranspilerProvider) Mothball(cat string) (*bytes.Reader, error) {
|
func (p TranspilerProvider) Mothball(cat string, w io.Writer) error {
|
||||||
c := transpile.NewFsCategory(p.fs, cat)
|
c := transpile.NewFsCategory(p.fs, cat)
|
||||||
return transpile.Mothball(c)
|
return transpile.Mothball(c, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maintain performs housekeeping.
|
// Maintain performs housekeeping.
|
||||||
|
|
|
@ -16,11 +16,13 @@ import (
|
||||||
|
|
||||||
// T represents the state of things
|
// T represents the state of things
|
||||||
type T struct {
|
type T struct {
|
||||||
Stdout io.Writer
|
Stdout io.Writer
|
||||||
Stderr io.Writer
|
Stderr io.Writer
|
||||||
Args []string
|
Args []string
|
||||||
BaseFs afero.Fs
|
BaseFs afero.Fs
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
|
|
||||||
|
// Arguments
|
||||||
filename string
|
filename string
|
||||||
answer string
|
answer string
|
||||||
}
|
}
|
||||||
|
@ -51,19 +53,21 @@ func (t *T) ParseArgs() (Command, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError)
|
flags := flag.NewFlagSet(t.Args[1], flag.ContinueOnError)
|
||||||
|
flags.SetOutput(t.Stderr)
|
||||||
directory := flags.String("dir", "", "Work directory")
|
directory := flags.String("dir", "", "Work directory")
|
||||||
|
|
||||||
switch t.Args[1] {
|
switch t.Args[1] {
|
||||||
case "mothball":
|
case "mothball":
|
||||||
cmd = t.DumpMothball
|
cmd = t.DumpMothball
|
||||||
|
flags.StringVar(&t.filename, "out", "", "Path to create mothball (empty for stdout)")
|
||||||
case "inventory":
|
case "inventory":
|
||||||
cmd = t.PrintInventory
|
cmd = t.PrintInventory
|
||||||
case "open":
|
case "open":
|
||||||
flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
|
|
||||||
cmd = t.DumpFile
|
cmd = t.DumpFile
|
||||||
|
flags.StringVar(&t.filename, "file", "puzzle.json", "Filename to open")
|
||||||
case "answer":
|
case "answer":
|
||||||
flags.StringVar(&t.answer, "answer", "", "Answer to check")
|
|
||||||
cmd = t.CheckAnswer
|
cmd = t.CheckAnswer
|
||||||
|
flags.StringVar(&t.answer, "answer", "", "Answer to check")
|
||||||
case "help":
|
case "help":
|
||||||
usage(t.Stderr)
|
usage(t.Stderr)
|
||||||
return nothing, nil
|
return nothing, nil
|
||||||
|
@ -73,12 +77,10 @@ func (t *T) ParseArgs() (Command, error) {
|
||||||
return nothing, fmt.Errorf("Invalid command")
|
return nothing, fmt.Errorf("Invalid command")
|
||||||
}
|
}
|
||||||
|
|
||||||
flags.SetOutput(t.Stderr)
|
|
||||||
if err := flags.Parse(t.Args[2:]); err != nil {
|
if err := flags.Parse(t.Args[2:]); err != nil {
|
||||||
return nothing, err
|
return nothing, err
|
||||||
}
|
}
|
||||||
if *directory != "" {
|
if *directory != "" {
|
||||||
log.Println(*directory)
|
|
||||||
t.fs = afero.NewBasePathFs(t.BaseFs, *directory)
|
t.fs = afero.NewBasePathFs(t.BaseFs, *directory)
|
||||||
} else {
|
} else {
|
||||||
t.fs = t.BaseFs
|
t.fs = t.BaseFs
|
||||||
|
@ -140,14 +142,30 @@ func (t *T) DumpFile() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DumpMothball writes a mothball to the writer.
|
// DumpMothball writes a mothball to the writer, or an output file if specified.
|
||||||
func (t *T) DumpMothball() error {
|
func (t *T) DumpMothball() error {
|
||||||
|
var w io.Writer
|
||||||
|
|
||||||
c := transpile.NewFsCategory(t.fs, "")
|
c := transpile.NewFsCategory(t.fs, "")
|
||||||
mb, err := transpile.Mothball(c)
|
|
||||||
if err != nil {
|
removeOnError := false
|
||||||
return err
|
switch t.filename {
|
||||||
|
case "", "-":
|
||||||
|
w = t.Stdout
|
||||||
|
default:
|
||||||
|
removeOnError = true
|
||||||
|
log.Println("Writing mothball to", t.filename)
|
||||||
|
outf, err := t.BaseFs.Create(t.filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outf.Close()
|
||||||
|
w = outf
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(t.Stdout, mb); err != nil {
|
if err := transpile.Mothball(c, w); err != nil {
|
||||||
|
if removeOnError {
|
||||||
|
t.BaseFs.Remove(t.filename)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -165,8 +183,6 @@ func (t *T) CheckAnswer() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// XXX: Convert puzzle.py to standalone thingies
|
|
||||||
|
|
||||||
t := &T{
|
t := &T{
|
||||||
Stdout: os.Stdout,
|
Stdout: os.Stdout,
|
||||||
Stderr: os.Stderr,
|
Stderr: os.Stderr,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dirtbags/moth/pkg/transpile"
|
"github.com/dirtbags/moth/pkg/transpile"
|
||||||
|
@ -83,16 +85,72 @@ func TestEverything(t *testing.T) {
|
||||||
if stdout.String() != "Moo." {
|
if stdout.String() != "Moo." {
|
||||||
t.Error("Wrong file pulled", stdout.String())
|
t.Error("Wrong file pulled", stdout.String())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMothballs(t *testing.T) {
|
||||||
|
stdout := new(bytes.Buffer)
|
||||||
|
stderr := new(bytes.Buffer)
|
||||||
|
tp := T{
|
||||||
|
Stdout: stdout,
|
||||||
|
Stderr: stderr,
|
||||||
|
BaseFs: newTestFs(),
|
||||||
|
}
|
||||||
|
|
||||||
stdout.Reset()
|
stdout.Reset()
|
||||||
if err := tp.Run("mothball", "-dir=unbroken"); err != nil {
|
if err := tp.Run("mothball", "-dir=unbroken", "-out=unbroken.mb"); err != nil {
|
||||||
t.Log(tp.fs)
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// afero.WriteFile(tp.BaseFs, "unbroken.mb", []byte("moo"), 0644)
|
||||||
|
fis, err := afero.ReadDir(tp.BaseFs, "/")
|
||||||
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
if stdout.Len() < 200 {
|
for _, fi := range fis {
|
||||||
t.Error("That's way too short to be a mothball")
|
t.Log(fi.Name())
|
||||||
}
|
}
|
||||||
if stdout.String()[:2] != "PK" {
|
|
||||||
t.Error("This mothball isn't a zip file!")
|
mb, err := tp.BaseFs.Open("unbroken.mb")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer mb.Close()
|
||||||
|
|
||||||
|
info, err := mb.Stat()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zmb, err := zip.NewReader(mb, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, zf := range zmb.File {
|
||||||
|
f, err := zf.Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
buf, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch zf.Name {
|
||||||
|
case "answers.txt":
|
||||||
|
if len(buf) == 0 {
|
||||||
|
t.Error("answers.txt empty")
|
||||||
|
}
|
||||||
|
case "puzzles.txt":
|
||||||
|
if len(buf) == 0 {
|
||||||
|
t.Error("puzzles.txt empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ Adjusting scores
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
rm /srv/moth/state/enabled # Suspend scoring
|
rm /srv/moth/state/enabled # Suspend scoring
|
||||||
nano /srv/moth/state/points.log
|
nano /srv/moth/state/points.log # Replace nano with your preferred editor
|
||||||
touch /srv/moth/state/enabled # Resume scoring
|
touch /srv/moth/state/enabled # Resume scoring
|
||||||
|
|
||||||
We don't warn participants before we do this:
|
We don't warn participants before we do this:
|
||||||
|
@ -78,10 +78,8 @@ any points scored while scoring is suspended are queued up and processed as soon
|
||||||
|
|
||||||
It's very important to suspend scoring before mucking around with the points log.
|
It's very important to suspend scoring before mucking around with the points log.
|
||||||
The maintenance loop assumes it is the only thing writing to this file,
|
The maintenance loop assumes it is the only thing writing to this file,
|
||||||
and any edits you make could blow aware points scored.
|
and any edits you make will remove points scored while you were editing.
|
||||||
|
|
||||||
No, I don't use nano.
|
|
||||||
None of us use nano.
|
|
||||||
|
|
||||||
|
|
||||||
Changing a team name
|
Changing a team name
|
||||||
|
|
|
@ -10,23 +10,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mothball packages a Category up for a production server run.
|
// Mothball packages a Category up for a production server run.
|
||||||
func Mothball(c Category) (*bytes.Reader, error) {
|
func Mothball(c Category, w io.Writer) error {
|
||||||
buf := new(bytes.Buffer)
|
zf := zip.NewWriter(w)
|
||||||
zf := zip.NewWriter(buf)
|
|
||||||
|
|
||||||
inv, err := c.Inventory()
|
inv, err := c.Inventory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
puzzlesTxt, err := zf.Create("puzzles.txt")
|
puzzlesTxt := new(bytes.Buffer)
|
||||||
if err != nil {
|
answersTxt := new(bytes.Buffer)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
answersTxt, err := zf.Create("answers.txt")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, points := range inv {
|
for _, points := range inv {
|
||||||
fmt.Fprintln(puzzlesTxt, points)
|
fmt.Fprintln(puzzlesTxt, points)
|
||||||
|
@ -34,11 +27,11 @@ func Mothball(c Category) (*bytes.Reader, error) {
|
||||||
puzzlePath := fmt.Sprintf("%d/puzzle.json", points)
|
puzzlePath := fmt.Sprintf("%d/puzzle.json", points)
|
||||||
pw, err := zf.Create(puzzlePath)
|
pw, err := zf.Create(puzzlePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
puzzle, err := c.Puzzle(points)
|
puzzle, err := c.Puzzle(points)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Puzzle %d: %s", points, err)
|
return fmt.Errorf("Puzzle %d: %s", points, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record answers in answers.txt
|
// Record answers in answers.txt
|
||||||
|
@ -55,7 +48,7 @@ func Mothball(c Category) (*bytes.Reader, error) {
|
||||||
// Write out Puzzle object
|
// Write out Puzzle object
|
||||||
penc := json.NewEncoder(pw)
|
penc := json.NewEncoder(pw)
|
||||||
if err := penc.Encode(puzzle); err != nil {
|
if err := penc.Encode(puzzle); err != nil {
|
||||||
return nil, fmt.Errorf("Puzzle %d: %s", points, err)
|
return fmt.Errorf("Puzzle %d: %s", points, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write out all attachments and scripts
|
// Write out all attachments and scripts
|
||||||
|
@ -64,20 +57,33 @@ func Mothball(c Category) (*bytes.Reader, error) {
|
||||||
attPath := fmt.Sprintf("%d/%s", points, att)
|
attPath := fmt.Sprintf("%d/%s", points, att)
|
||||||
aw, err := zf.Create(attPath)
|
aw, err := zf.Create(attPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
ar, err := c.Open(points, att)
|
ar, err := c.Open(points, att)
|
||||||
if exerr, ok := err.(*exec.ExitError); ok {
|
if exerr, ok := err.(*exec.ExitError); ok {
|
||||||
return nil, fmt.Errorf("Puzzle %d: %s: %s: %s", points, att, err, string(exerr.Stderr))
|
return fmt.Errorf("Puzzle %d: %s: %s: %s", points, att, err, string(exerr.Stderr))
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
|
return fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
|
||||||
}
|
}
|
||||||
if _, err := io.Copy(aw, ar); err != nil {
|
if _, err := io.Copy(aw, ar); err != nil {
|
||||||
return nil, fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
|
return fmt.Errorf("Puzzle %d: %s: %s", points, att, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pf, err := zf.Create("puzzles.txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
puzzlesTxt.WriteTo(pf)
|
||||||
|
|
||||||
|
af, err := zf.Create("answers.txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
answersTxt.WriteTo(af)
|
||||||
|
|
||||||
zf.Close()
|
zf.Close()
|
||||||
|
|
||||||
return bytes.NewReader(buf.Bytes()), nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package transpile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
@ -14,7 +15,8 @@ import (
|
||||||
|
|
||||||
func TestMothballsMemFs(t *testing.T) {
|
func TestMothballsMemFs(t *testing.T) {
|
||||||
static := NewFsCategory(newTestFs(), "cat1")
|
static := NewFsCategory(newTestFs(), "cat1")
|
||||||
if _, err := Mothball(static); err != nil {
|
mb := new(bytes.Buffer)
|
||||||
|
if err := Mothball(static, mb); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,13 +27,15 @@ func TestMothballsOsFs(t *testing.T) {
|
||||||
|
|
||||||
fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata")
|
fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata")
|
||||||
static := NewFsCategory(fs, "static")
|
static := NewFsCategory(fs, "static")
|
||||||
mb, err := Mothball(static)
|
mb := new(bytes.Buffer)
|
||||||
|
err := Mothball(static, mb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mbr, err := zip.NewReader(mb, int64(mb.Len()))
|
mbReader := bytes.NewReader(mb.Bytes())
|
||||||
|
mbr, err := zip.NewReader(mbReader, int64(mb.Len()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -43,7 +47,7 @@ func TestMothballsOsFs(t *testing.T) {
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
if buf, err := ioutil.ReadAll(f); err != nil {
|
if buf, err := ioutil.ReadAll(f); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
} else if string(buf) != "" {
|
} else if string(buf) != "1\n2\n3\n" {
|
||||||
t.Error("Bad puzzles.txt", string(buf))
|
t.Error("Bad puzzles.txt", string(buf))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ function renderPuzzles(obj) {
|
||||||
pdiv.appendChild(l)
|
pdiv.appendChild(l)
|
||||||
for (let puzzle of puzzles) {
|
for (let puzzle of puzzles) {
|
||||||
let points = puzzle
|
let points = puzzle
|
||||||
let id = puzzle
|
let id = null
|
||||||
|
|
||||||
if (Array.isArray(puzzle)) {
|
if (Array.isArray(puzzle)) {
|
||||||
points = puzzle[0]
|
points = puzzle[0]
|
||||||
|
@ -80,7 +80,7 @@ function renderPuzzles(obj) {
|
||||||
let url = new URL("puzzle.html", window.location)
|
let url = new URL("puzzle.html", window.location)
|
||||||
url.searchParams.set("cat", cat)
|
url.searchParams.set("cat", cat)
|
||||||
url.searchParams.set("points", points)
|
url.searchParams.set("points", points)
|
||||||
url.searchParams.set("pid", id)
|
if (id) { url.searchParams.set("pid", id) }
|
||||||
a.href = url.toString()
|
a.href = url.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ function renderPuzzles(obj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderState(obj) {
|
function renderState(obj) {
|
||||||
|
window.state = obj
|
||||||
devel = obj.Config.Devel
|
devel = obj.Config.Devel
|
||||||
if (devel) {
|
if (devel) {
|
||||||
let params = new URLSearchParams(window.location.search)
|
let params = new URLSearchParams(window.location.search)
|
||||||
|
|
|
@ -1,255 +0,0 @@
|
||||||
{
|
|
||||||
"__comment__": [
|
|
||||||
"This file is to help debug themes.",
|
|
||||||
"MOTHd will ignore it."
|
|
||||||
],
|
|
||||||
"teams": {
|
|
||||||
"0": "4HED Followers",
|
|
||||||
"1": "Dirtbags",
|
|
||||||
"17": "Eyeball",
|
|
||||||
"2": "Soup Giver!!!!!!!!!",
|
|
||||||
"24": "Dumb freshmans 3",
|
|
||||||
"25": "Winner",
|
|
||||||
"2d": "Cool team name",
|
|
||||||
"2f": "Dumm freshmans #1",
|
|
||||||
"4": "K19 the Widow Maker",
|
|
||||||
"5": "2T2",
|
|
||||||
"6": "Apples",
|
|
||||||
"7": "Top Minds",
|
|
||||||
"8": "DIRTBAGS",
|
|
||||||
"b": "Antiderivative of Pizza"
|
|
||||||
},
|
|
||||||
"points": [
|
|
||||||
[1573007086,"0","codebreaking",1],
|
|
||||||
[1573007096,"1","codebreaking",1],
|
|
||||||
[1573007114,"2","codebreaking",1],
|
|
||||||
[1573007153,"0","codebreaking",2],
|
|
||||||
[1573007159,"4","codebreaking",1],
|
|
||||||
[1573007169,"5","codebreaking",1],
|
|
||||||
[1573007181,"6","sequence",1],
|
|
||||||
[1573007184,"7","codebreaking",1],
|
|
||||||
[1573007209,"8","codebreaking",1],
|
|
||||||
[1573007212,"2","codebreaking",2],
|
|
||||||
[1573007240,"1","sequence",1],
|
|
||||||
[1573007244,"b","codebreaking",1],
|
|
||||||
[1573007246,"1","nocode",1],
|
|
||||||
[1573007258,"5","nocode",1],
|
|
||||||
[1573007271,"5","nocode",2],
|
|
||||||
[1573007284,"1","steg",1],
|
|
||||||
[1573007295,"7","codebreaking",2],
|
|
||||||
[1573007298,"2","codebreaking",4],
|
|
||||||
[1573007305,"5","nocode",3],
|
|
||||||
[1573007316,"7","codebreaking",4],
|
|
||||||
[1573007321,"0","codebreaking",4],
|
|
||||||
[1573007328,"5","nocode",4],
|
|
||||||
[1573007331,"7","nocode",1],
|
|
||||||
[1573007336,"17","codebreaking",1],
|
|
||||||
[1573007340,"7","nocode",2],
|
|
||||||
[1573007367,"0","nocode",10],
|
|
||||||
[1573007369,"7","nocode",3],
|
|
||||||
[1573007371,"b","nocode",1],
|
|
||||||
[1573007379,"7","nocode",4],
|
|
||||||
[1573007388,"b","nocode",2],
|
|
||||||
[1573007391,"6","sequence",2],
|
|
||||||
[1573007397,"4","codebreaking",2],
|
|
||||||
[1573007407,"b","nocode",3],
|
|
||||||
[1573007411,"7","nocode",10],
|
|
||||||
[1573007413,"5","nocode",10],
|
|
||||||
[1573007429,"b","nocode",4],
|
|
||||||
[1573007442,"24","codebreaking",2],
|
|
||||||
[1573007451,"25","codebreaking",1],
|
|
||||||
[1573007456,"7","sequence",1],
|
|
||||||
[1573007460,"b","nocode",10],
|
|
||||||
[1573007467,"5","sequence",1],
|
|
||||||
[1573007471,"7","sequence",2],
|
|
||||||
[1573007478,"5","sequence",2],
|
|
||||||
[1573007479,"17","codebreaking",2],
|
|
||||||
[1573007490,"2","codebreaking",5],
|
|
||||||
[1573007509,"2d","codebreaking",1],
|
|
||||||
[1573007536,"8","codebreaking",2],
|
|
||||||
[1573007544,"2f","codebreaking",1],
|
|
||||||
[1573007546,"b","sequence",1],
|
|
||||||
[1573007574,"24","codebreaking",4],
|
|
||||||
[1573007581,"b","sequence",2],
|
|
||||||
[1573007591,"25","codebreaking",2],
|
|
||||||
[1573007603,"8","codebreaking",4],
|
|
||||||
[1573007614,"0","nocode",20],
|
|
||||||
[1573007639,"4","codebreaking",4],
|
|
||||||
[1573007678,"6","codebreaking",1],
|
|
||||||
[1573007692,"8","nocode",1],
|
|
||||||
[1573007695,"24","codebreaking",5],
|
|
||||||
[1573007705,"7","codebreaking",5],
|
|
||||||
[1573007707,"8","nocode",2],
|
|
||||||
[1573007713,"17","nocode",1],
|
|
||||||
[1573007727,"17","nocode",2],
|
|
||||||
[1573007735,"8","nocode",3],
|
|
||||||
[1573007737,"b","steg",1],
|
|
||||||
[1573007739,"25","codebreaking",4],
|
|
||||||
[1573007749,"8","nocode",4],
|
|
||||||
[1573007757,"17","codebreaking",4],
|
|
||||||
[1573007768,"8","nocode",10],
|
|
||||||
[1573007795,"0","sequence",1],
|
|
||||||
[1573007799,"8","sequence",1],
|
|
||||||
[1573007816,"0","sequence",2],
|
|
||||||
[1573007822,"8","sequence",2],
|
|
||||||
[1573007834,"24","codebreaking",6],
|
|
||||||
[1573007853,"2d","codebreaking",2],
|
|
||||||
[1573007905,"1","codebreaking",2],
|
|
||||||
[1573007941,"4","codebreaking",5],
|
|
||||||
[1573007956,"1","codebreaking",4],
|
|
||||||
[1573007974,"6","codebreaking",2],
|
|
||||||
[1573007998,"17","sequence",1],
|
|
||||||
[1573008022,"b","codebreaking",4],
|
|
||||||
[1573008055,"24","sequence",2],
|
|
||||||
[1573008063,"6","codebreaking",4],
|
|
||||||
[1573008066,"2d","codebreaking",4],
|
|
||||||
[1573008074,"24","sequence",1],
|
|
||||||
[1573008099,"17","nocode",4],
|
|
||||||
[1573008101,"0","codebreaking",7],
|
|
||||||
[1573008108,"2d","nocode",1],
|
|
||||||
[1573008135,"24","nocode",30],
|
|
||||||
[1573008146,"1","codebreaking",5],
|
|
||||||
[1573008162,"2d","nocode",2],
|
|
||||||
[1573008174,"b","codebreaking",2],
|
|
||||||
[1573008191,"2","codebreaking",6],
|
|
||||||
[1573008234,"6","codebreaking",5],
|
|
||||||
[1573008240,"2","nocode",10],
|
|
||||||
[1573008291,"5","steg",1],
|
|
||||||
[1573008310,"6","nocode",1],
|
|
||||||
[1573008323,"2d","nocode",3],
|
|
||||||
[1573008327,"6","nocode",2],
|
|
||||||
[1573008330,"25","codebreaking",5],
|
|
||||||
[1573008334,"2f","codebreaking",2],
|
|
||||||
[1573008348,"6","nocode",3],
|
|
||||||
[1573008356,"2d","nocode",4],
|
|
||||||
[1573008362,"b","codebreaking",5],
|
|
||||||
[1573008364,"6","nocode",4],
|
|
||||||
[1573008364,"17","codebreaking",5],
|
|
||||||
[1573008371,"24","nocode",4],
|
|
||||||
[1573008385,"24","nocode",3],
|
|
||||||
[1573008390,"6","nocode",10],
|
|
||||||
[1573008397,"24","nocode",2],
|
|
||||||
[1573008400,"25","nocode",1],
|
|
||||||
[1573008402,"2d","nocode",10],
|
|
||||||
[1573008408,"24","nocode",1],
|
|
||||||
[1573008419,"25","nocode",2],
|
|
||||||
[1573008429,"24","steg",1],
|
|
||||||
[1573008437,"25","nocode",3],
|
|
||||||
[1573008451,"25","nocode",4],
|
|
||||||
[1573008479,"25","nocode",10],
|
|
||||||
[1573008502,"2d","sequence",1],
|
|
||||||
[1573008506,"17","codebreaking",6],
|
|
||||||
[1573008537,"2d","sequence",2],
|
|
||||||
[1573008649,"17","codebreaking",7],
|
|
||||||
[1573008668,"2f","codebreaking",4],
|
|
||||||
[1573008716,"1","codebreaking",6],
|
|
||||||
[1573008768,"8","steg",1],
|
|
||||||
[1573008808,"7","nocode",50],
|
|
||||||
[1573008817,"24","steg",2],
|
|
||||||
[1573008832,"2f","codebreaking",5],
|
|
||||||
[1573008890,"17","steg",1],
|
|
||||||
[1573008902,"b","steg",2],
|
|
||||||
[1573008932,"7","steg",1],
|
|
||||||
[1573008944,"24","steg",3],
|
|
||||||
[1573008978,"2","steg",1],
|
|
||||||
[1573009006,"24","steg",4],
|
|
||||||
[1573009032,"6","steg",1],
|
|
||||||
[1573009038,"b","steg",3],
|
|
||||||
[1573009052,"2d","codebreaking",5],
|
|
||||||
[1573009098,"b","steg",4],
|
|
||||||
[1573009122,"8","steg",2],
|
|
||||||
[1573009125,"4","nocode",1],
|
|
||||||
[1573009160,"24","nocode",10],
|
|
||||||
[1573009161,"4","nocode",2],
|
|
||||||
[1573009179,"2","steg",2],
|
|
||||||
[1573009180,"1","steg",2],
|
|
||||||
[1573009194,"24","nocode",20],
|
|
||||||
[1573009203,"0","nocode",50],
|
|
||||||
[1573009212,"2f","codebreaking",6],
|
|
||||||
[1573009240,"2f","nocode",1],
|
|
||||||
[1573009250,"4","nocode",4],
|
|
||||||
[1573009255,"2f","nocode",2],
|
|
||||||
[1573009258,"2","steg",4],
|
|
||||||
[1573009282,"4","nocode",10],
|
|
||||||
[1573009299,"25","sequence",1],
|
|
||||||
[1573009305,"6","steg",4],
|
|
||||||
[1573009308,"17","steg",3],
|
|
||||||
[1573009310,"1","steg",3],
|
|
||||||
[1573009334,"7","steg",4],
|
|
||||||
[1573009345,"1","steg",4],
|
|
||||||
[1573009345,"7","steg",3],
|
|
||||||
[1573009354,"8","steg",4],
|
|
||||||
[1573009357,"25","sequence",2],
|
|
||||||
[1573009402,"6","steg",3],
|
|
||||||
[1573009402,"b","sequence",8],
|
|
||||||
[1573009413,"2f","nocode",3],
|
|
||||||
[1573009437,"17","steg",2],
|
|
||||||
[1573009455,"2f","nocode",10],
|
|
||||||
[1573009481,"b","sequence",16],
|
|
||||||
[1573009502,"b","sequence",19],
|
|
||||||
[1573009520,"b","sequence",25],
|
|
||||||
[1573009525,"17","steg",4],
|
|
||||||
[1573009559,"7","steg",2],
|
|
||||||
[1573009561,"b","sequence",35],
|
|
||||||
[1573009571,"0","sequence",35],
|
|
||||||
[1573009588,"25","steg",1],
|
|
||||||
[1573009602,"24","sequence",8],
|
|
||||||
[1573009607,"2","steg",5],
|
|
||||||
[1573009614,"1","steg",5],
|
|
||||||
[1573009617,"17","sequence",35],
|
|
||||||
[1573009620,"7","sequence",50],
|
|
||||||
[1573009621,"6","steg",5],
|
|
||||||
[1573009629,"5","steg",3],
|
|
||||||
[1573009632,"7","sequence",35],
|
|
||||||
[1573009644,"17","sequence",25],
|
|
||||||
[1573009670,"6","steg",6],
|
|
||||||
[1573009698,"8","steg",6],
|
|
||||||
[1573009700,"17","sequence",19],
|
|
||||||
[1573009703,"24","steg",6],
|
|
||||||
[1573009703,"4","sequence",1],
|
|
||||||
[1573009707,"0","sequence",50],
|
|
||||||
[1573009710,"25","steg",2],
|
|
||||||
[1573009729,"2f","sequence",1],
|
|
||||||
[1573009768,"1","steg",6],
|
|
||||||
[1573009814,"2","codebreaking",8],
|
|
||||||
[1573009842,"0","steg",1],
|
|
||||||
[1573009844,"2f","sequence",2],
|
|
||||||
[1573009882,"4","steg",1],
|
|
||||||
[1573009896,"25","steg",3],
|
|
||||||
[1573009931,"1","sequence",2],
|
|
||||||
[1573009937,"25","steg",4],
|
|
||||||
[1573010066,"7","steg",6],
|
|
||||||
[1573010101,"25","steg",5],
|
|
||||||
[1573010114,"5","steg",4],
|
|
||||||
[1573010137,"25","steg",6],
|
|
||||||
[1573010185,"4","sequence",2],
|
|
||||||
[1573010229,"17","nocode",80],
|
|
||||||
[1573010256,"24","sequence",35],
|
|
||||||
[1573010281,"6","codebreaking",7],
|
|
||||||
[1573010336,"25","codebreaking",6],
|
|
||||||
[1573010390,"7","codebreaking",7],
|
|
||||||
[1573010468,"2f","steg",1],
|
|
||||||
[1573010712,"0","steg",2],
|
|
||||||
[1573010739,"0","steg",3],
|
|
||||||
[1573010754,"0","steg",4],
|
|
||||||
[1573010778,"0","steg",5],
|
|
||||||
[1573010784,"7","nocode",90],
|
|
||||||
[1573010792,"0","steg",6],
|
|
||||||
[1573011760,"7","sequence",60],
|
|
||||||
[1573056120,"0","sequence",100],
|
|
||||||
[1573056324,"0","sequence",200],
|
|
||||||
[1573056791,"0","sequence",300],
|
|
||||||
[1573057092,"0","sequence",400],
|
|
||||||
[1573076767,"25","sequence",400],
|
|
||||||
[1573076809,"25","sequence",300],
|
|
||||||
[1573076838,"25","sequence",200],
|
|
||||||
[1573076936,"25","nocode",20],
|
|
||||||
[1573077275,"25","nocode",50],
|
|
||||||
[1573078364,"0","sequence",19],
|
|
||||||
[1573078432,"0","sequence",25],
|
|
||||||
[1573078487,"25","sequence",35],
|
|
||||||
[1573078501,"25","sequence",50],
|
|
||||||
[1573079359,"0","nocode",90],
|
|
||||||
[1573079714,"25","nocode",9]
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -132,7 +132,7 @@ async function loadPuzzle(categoryName, points, puzzleId) {
|
||||||
document.getElementById("authors").textContent = window.puzzle.Pre.Authors.join(", ")
|
document.getElementById("authors").textContent = window.puzzle.Pre.Authors.join(", ")
|
||||||
|
|
||||||
// If answers are provided, this is the devel server
|
// If answers are provided, this is the devel server
|
||||||
if (window.puzzle.Answers) {
|
if (window.puzzle.Answers.length > 0) {
|
||||||
devel_addin(document.getElementById("devel"))
|
devel_addin(document.getElementById("devel"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,8 +204,8 @@ function init() {
|
||||||
let points = params.get("points")
|
let points = params.get("points")
|
||||||
let puzzleId = params.get("pid")
|
let puzzleId = params.get("pid")
|
||||||
|
|
||||||
if (categoryName && points && puzzleId) {
|
if (categoryName && points) {
|
||||||
loadPuzzle(categoryName, points, puzzleId)
|
loadPuzzle(categoryName, points, puzzleId || points)
|
||||||
}
|
}
|
||||||
|
|
||||||
let teamId = sessionStorage.getItem("id")
|
let teamId = sessionStorage.getItem("id")
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
{
|
|
||||||
"__comment__": [
|
|
||||||
"This file is to help debug themes.",
|
|
||||||
"MOTHd will ignore it."
|
|
||||||
],
|
|
||||||
"codebreaking": [
|
|
||||||
[1,"37117e6b034696b86c6516477cc0bc60bc1e642e"],
|
|
||||||
[2,"546b586428979771b061608489327da4940086a7"],
|
|
||||||
[4,"6f2a33c93f56b4f29cc79e6576ba4d1000aa1756"],
|
|
||||||
[5,"c654fe263909b1940d7aad8c572363a0569c07c6"],
|
|
||||||
[6,"f30bd32bf940f2bb03506ec334d2d204efc4695b"],
|
|
||||||
[7,"128b119083b6ae70c380a8eb70ec6a518425e7af"],
|
|
||||||
[8,"edd4f57aeb565b3b053fa194f5e677cb77ef0285"],
|
|
||||||
[15,"9781863bca9f596972e2a10460932ec5ec6be3fe"]
|
|
||||||
],
|
|
||||||
"nocode": [
|
|
||||||
[1,"37117e6b034696b86c6516477cc0bc60bc1e642e"],
|
|
||||||
[2,"546b586428979771b061608489327da4940086a7"],
|
|
||||||
[3,"79c08697a1923da1118fd0c2e922b5d3899cabcc"],
|
|
||||||
[4,"6f2a33c93f56b4f29cc79e6576ba4d1000aa1756"],
|
|
||||||
[10,"bf4fae263bf6e4243b143f4ecd64e471f3ec75dd"],
|
|
||||||
[20,"9f374f6dac9f972fac4693099a7bfa7c535f7503"],
|
|
||||||
[30,"02de1196d43976b2d050c6c597f068623d2df201"],
|
|
||||||
[50,"9acb3af947cb4aa10a9c1221c04518f956cdc0d0"],
|
|
||||||
[80,"78f807ac44f3cbf537861e7cdf1ac53937e4ee47"],
|
|
||||||
[90,"6d537653aa599178c72528f7e1f2fbb36e6333f9"],
|
|
||||||
[100,"4f5982a3a7cc9b9af0320130132e8cab39a1fd2c"]
|
|
||||||
],
|
|
||||||
"sequence": [
|
|
||||||
[1,"37117e6b034696b86c6516477cc0bc60bc1e642e"],
|
|
||||||
[2,"546b586428979771b061608489327da4940086a7"],
|
|
||||||
[8,"edd4f57aeb565b3b053fa194f5e677cb77ef0285"],
|
|
||||||
[16,"a9ace4b773f045c422260edefaa8563dcd80ac59"],
|
|
||||||
[19,"f11ca0172451f37ba6f4d66ff9add80013480a49"],
|
|
||||||
[25,"0458533d28705548829e53d686215cc6fbeec8f5"],
|
|
||||||
[35,"91aac06bae090ae7d1699b5a78601ef8d29e9271"],
|
|
||||||
[50,"9acb3af947cb4aa10a9c1221c04518f956cdc0d0"],
|
|
||||||
[60,"bf84beed9e382268ab40d0113dfeb73c96aa919a"],
|
|
||||||
[100,"4f5982a3a7cc9b9af0320130132e8cab39a1fd2c"],
|
|
||||||
[200,"3b9b8993fe639cf0c19a58b39ebbf6077828887a"],
|
|
||||||
[300,"0f13c4d19bc5d2e10d43e8cd2e40f759e731cece"],
|
|
||||||
[400,"db7a59f313818fc9598969d2a0a04e21bd26697f"],
|
|
||||||
[500,"81c5389eb5406aa44053662f6482f246b8a12e0c"]
|
|
||||||
],
|
|
||||||
"steg": [
|
|
||||||
[1,"200e8cd902ba7304765c463f6ed1322bc25f3454"],
|
|
||||||
[2,"707328988c3986d450d8fe419eb49f078fb7998c"],
|
|
||||||
[3,"d0b336ad59cbcd4415ddf200c6c099db5c3fea1d"],
|
|
||||||
[4,"f071503b403ffee2b38e186e800bfd5dd28e8f0e"],
|
|
||||||
[5,"186f425fa5762ef37f874cc602fe0edc4325a5d2"],
|
|
||||||
[6,"c6527c3c30c4e6a33026192d358d83d259cd17a7"],
|
|
||||||
[10,"84973f77a1b14e4666f3d8a8bdeead7633c4ed56"]
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -13,6 +13,8 @@ function scoreboardInit() {
|
||||||
]
|
]
|
||||||
|
|
||||||
function update(state) {
|
function update(state) {
|
||||||
|
window.state = state
|
||||||
|
|
||||||
for (let rotate of document.querySelectorAll(".rotate")) {
|
for (let rotate of document.querySelectorAll(".rotate")) {
|
||||||
rotate.appendChild(rotate.firstElementChild)
|
rotate.appendChild(rotate.firstElementChild)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue