diff -u -Nr teeworlds-0.7.3.1-src.orig/CMakeLists.txt teeworlds-0.7.3.1-src/CMakeLists.txt
--- teeworlds-0.7.3.1-src.orig/CMakeLists.txt	2019-09-04 20:10:59.814690105 +0300
+++ teeworlds-0.7.3.1-src/CMakeLists.txt	2019-09-04 20:11:17.910689932 +0300
@@ -1481,6 +1481,8 @@
   gamemodes/tdm.h
   gameworld.cpp
   gameworld.h
+  maprotation.cpp
+  maprotation.h
   player.cpp
   player.h
 )
diff -u -Nr teeworlds-0.7.3.1-src.orig/src/game/server/gamecontext.cpp teeworlds-0.7.3.1-src/src/game/server/gamecontext.cpp
--- teeworlds-0.7.3.1-src.orig/src/game/server/gamecontext.cpp	2019-09-04 20:10:59.824690105 +0300
+++ teeworlds-0.7.3.1-src/src/game/server/gamecontext.cpp	2019-09-04 20:14:09.514688296 +0300
@@ -1393,6 +1393,11 @@
 	pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf);
 }
 
+void CGameContext::ConNextMap(IConsole::IResult *pResult, void *pUserData)
+{
+	((CGameContext *)pUserData)->m_pController->NextMap();
+}
+
 void CGameContext::ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData)
 {
 	pfnCallback(pResult, pCallbackUserData);
@@ -1451,6 +1456,7 @@
 	Console()->Register("remove_vote", "s", CFGFLAG_SERVER, ConRemoveVote, this, "remove a voting option");
 	Console()->Register("clear_votes", "", CFGFLAG_SERVER, ConClearVotes, this, "Clears the voting options");
 	Console()->Register("vote", "r", CFGFLAG_SERVER, ConVote, this, "Force a vote to yes/no");
+	Console()->Register("next_map", "", CFGFLAG_SERVER, ConNextMap, this, "Switch to another random map");
 }
 
 void CGameContext::OnInit()
diff -u -Nr teeworlds-0.7.3.1-src.orig/src/game/server/gamecontext.h teeworlds-0.7.3.1-src/src/game/server/gamecontext.h
--- teeworlds-0.7.3.1-src.orig/src/game/server/gamecontext.h	2019-09-04 20:10:59.824690105 +0300
+++ teeworlds-0.7.3.1-src/src/game/server/gamecontext.h	2019-09-04 20:14:40.854687997 +0300
@@ -60,6 +60,7 @@
 	static void ConRemoveVote(IConsole::IResult *pResult, void *pUserData);
 	static void ConClearVotes(IConsole::IResult *pResult, void *pUserData);
 	static void ConVote(IConsole::IResult *pResult, void *pUserData);
+	static void ConNextMap(IConsole::IResult *pResult, void *pUserData);
 	static void ConchainSpecialMotdupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
 	static void ConchainSettingUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
 	static void ConchainGameinfoUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData);
diff -u -Nr teeworlds-0.7.3.1-src.orig/src/game/server/gamecontroller.cpp teeworlds-0.7.3.1-src/src/game/server/gamecontroller.cpp
--- teeworlds-0.7.3.1-src.orig/src/game/server/gamecontroller.cpp	2019-09-04 20:10:59.824690105 +0300
+++ teeworlds-0.7.3.1-src/src/game/server/gamecontroller.cpp	2019-09-04 20:15:10.877687710 +0300
@@ -10,6 +10,8 @@
 #include "gamecontroller.h"
 #include "player.h"
 
+#include "maprotation.h"
+
 
 IGameController::IGameController(CGameContext *pGameServer)
 {
@@ -953,58 +955,77 @@
 	if(!str_length(g_Config.m_SvMaprotation))
 		return;
 
+	NextMap();
+}
+
+void IGameController::NextMap()
+{
 	// handle maprotation
 	const char *pMapRotation = g_Config.m_SvMaprotation;
 	const char *pCurrentMap = g_Config.m_SvMap;
 
-	int CurrentMapLen = str_length(pCurrentMap);
-	const char *pNextMap = pMapRotation;
-	while(*pNextMap)
-	{
-		int WordLen = 0;
-		while(pNextMap[WordLen] && !IsSeparator(pNextMap[WordLen]))
-			WordLen++;
-
-		if(WordLen == CurrentMapLen && str_comp_num(pNextMap, pCurrentMap, CurrentMapLen) == 0)
-		{
-			// map found
-			pNextMap += CurrentMapLen;
-			while(*pNextMap && IsSeparator(*pNextMap))
-				pNextMap++;
-
-			break;
-		}
+	const char *pNextMap;
+	char aBuf[512];
 
-		pNextMap++;
+	if(str_comp_num(pMapRotation, "!random ", 8) == 0)
+	{
+		// random rotation
+		static IMapRotation *rotation = RandomMapRotation();
+		pNextMap = rotation->NextMap(pMapRotation + 8, pCurrentMap);
 	}
-
-	// restart rotation
-	if(pNextMap[0] == 0)
+	else
+	{
+		// standard rotation
+		int CurrentMapLen = str_length(pCurrentMap);
 		pNextMap = pMapRotation;
+		while(*pNextMap)
+		{
+			int WordLen = 0;
+			while(pNextMap[WordLen] && !IsSeparator(pNextMap[WordLen]))
+				WordLen++;
+
+			if(WordLen == CurrentMapLen && str_comp_num(pNextMap, pCurrentMap, CurrentMapLen) == 0)
+			{
+				// map found
+				pNextMap += CurrentMapLen;
+				while(*pNextMap && IsSeparator(*pNextMap))
+					pNextMap++;
 
-	// cut out the next map
-	char aBuf[512] = {0};
-	for(int i = 0; i < 511; i++)
-	{
-		aBuf[i] = pNextMap[i];
-		if(IsSeparator(pNextMap[i]) || pNextMap[i] == 0)
+				break;
+			}
+
+			pNextMap++;
+		}
+
+		// restart rotation
+		if(pNextMap[0] == 0)
+			pNextMap = pMapRotation;
+
+		// cut out the next map
+		for(int i = 0; i < 511; i++)
 		{
-			aBuf[i] = 0;
-			break;
+			aBuf[i] = pNextMap[i];
+			if(IsSeparator(pNextMap[i]) || pNextMap[i] == 0)
+			{
+				aBuf[i] = 0;
+				break;
+			}
 		}
-	}
 
-	// skip spaces
-	int i = 0;
-	while(IsSeparator(aBuf[i]))
-		i++;
+		// skip spaces
+		int i = 0;
+		while(IsSeparator(aBuf[i]))
+			i++;
+
+		pNextMap = &aBuf[i];
+	}
 
 	m_MatchCount = 0;
 
 	char aBufMsg[256];
-	str_format(aBufMsg, sizeof(aBufMsg), "rotating map to %s", &aBuf[i]);
-	GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "game", aBuf);
-	str_copy(g_Config.m_SvMap, &aBuf[i], sizeof(g_Config.m_SvMap));
+	str_format(aBufMsg, sizeof(aBufMsg), "rotating map to %s", pNextMap);
+	GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "game", pNextMap);
+	str_copy(g_Config.m_SvMap, pNextMap, sizeof(g_Config.m_SvMap));
 }
 
 // spawn
diff -u -Nr teeworlds-0.7.3.1-src.orig/src/game/server/gamecontroller.h teeworlds-0.7.3.1-src/src/game/server/gamecontroller.h
--- teeworlds-0.7.3.1-src.orig/src/game/server/gamecontroller.h	2019-09-04 20:10:59.824690105 +0300
+++ teeworlds-0.7.3.1-src/src/game/server/gamecontroller.h	2019-09-04 20:15:41.390687419 +0300
@@ -214,6 +214,8 @@
 	
 	int GetRealPlayerNum() const { return m_aTeamSize[TEAM_RED]+m_aTeamSize[TEAM_BLUE]; }
 	int GetStartTeam();
+
+	void NextMap();
 };
 
 #endif
diff -u -Nr teeworlds-0.7.3.1-src.orig/src/game/server/maprotation.cpp teeworlds-0.7.3.1-src/src/game/server/maprotation.cpp
--- teeworlds-0.7.3.1-src.orig/src/game/server/maprotation.cpp	1970-01-01 03:00:00.000000000 +0300
+++ teeworlds-0.7.3.1-src/src/game/server/maprotation.cpp	2019-09-04 20:16:16.310687086 +0300
@@ -0,0 +1,123 @@
+#include "maprotation.h"
+
+#include <vector>
+#include <iostream>
+#include <fstream>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#include <base/system.h>
+#include <base/math.h>
+
+class CRandomMapRotation: public IMapRotation
+{
+public:
+	class CMap
+	{
+	public:
+		const char* m_pName;
+		float m_Probability;
+
+		CMap(const char* name, float weight);
+	};
+	std::vector<CMap> m_MapList;
+	const char* m_pMapFile;
+	time_t m_MapFileMTime;
+
+	CRandomMapRotation();
+
+	virtual const char* NextMap(const char* rotation, const char* current);
+	void UpdateList(const char* rotation);
+	void ClearList();
+};
+
+time_t GetMTime(const char* file)
+{
+	struct stat st;
+	if (stat(file, &st) < 0)
+	{
+		dbg_msg("rotation", "Cannot stat %s: %s", file, strerror(errno));
+		return -1;
+	}
+	return st.st_mtime;
+}
+
+CRandomMapRotation::CMap::CMap(const char* name, float probability)
+: m_pName(name), m_Probability(probability) {}
+
+CRandomMapRotation::CRandomMapRotation()
+: m_pMapFile(NULL) {};
+
+void CRandomMapRotation::UpdateList(const char* rotation)
+{
+	if (m_pMapFile && str_comp(rotation, m_pMapFile) == 0)
+	{
+		time_t mtime = GetMTime(m_pMapFile);
+		if (mtime < 0 || mtime == m_MapFileMTime)
+			return;
+		m_MapFileMTime = mtime;
+	}
+	else
+	{
+		time_t mtime = GetMTime(rotation);
+		if (mtime < 0) return;
+		m_pMapFile = rotation;
+		m_MapFileMTime = mtime;
+	}
+
+	ClearList();
+
+	std::ifstream f(m_pMapFile);
+	while (1)
+	{
+		char buf[128];
+		float weight;
+		f >> buf >> weight;
+		if (!f) break;
+		char *name = new char[strlen(buf) + 1];
+		if (!name) break;
+		strcpy(name, buf);
+		m_MapList.push_back(CMap(name, weight));
+	}
+
+	// normalize probabilities
+	float norm = 0;
+	for (std::vector<CMap>::const_iterator m = m_MapList.begin(); m != m_MapList.end(); ++m)
+		norm += m->m_Probability;
+	for (std::vector<CMap>::iterator m = m_MapList.begin(); m != m_MapList.end(); ++m)
+		m->m_Probability /= norm;
+
+	for (std::vector<CMap>::const_iterator m = m_MapList.begin(); m != m_MapList.end(); ++m)
+		dbg_msg("rotation", "registering map %s with probability %g", m->m_pName, m->m_Probability);
+}
+
+void CRandomMapRotation::ClearList()
+{
+	for (std::vector<CMap>::const_iterator m = m_MapList.begin(); m != m_MapList.end(); ++m)
+		delete[] m->m_pName;
+	m_MapList.clear();
+}
+
+const char* CRandomMapRotation::NextMap(const char* rotation, const char* current)
+{
+	UpdateList(rotation);
+	if (m_MapList.empty()) return current;
+	float w = frandom();
+	float x = 0;
+	for (std::vector<CMap>::const_iterator m = m_MapList.begin(); m != m_MapList.end(); ++m)
+	{
+		x += m->m_Probability;
+		if (x >= w) return m->m_pName;
+	}
+	// should never happen
+	return current;
+}
+
+IMapRotation *RandomMapRotation()
+{
+	return new CRandomMapRotation;
+}
diff -u -Nr teeworlds-0.7.3.1-src.orig/src/game/server/maprotation.h teeworlds-0.7.3.1-src/src/game/server/maprotation.h
--- teeworlds-0.7.3.1-src.orig/src/game/server/maprotation.h	1970-01-01 03:00:00.000000000 +0300
+++ teeworlds-0.7.3.1-src/src/game/server/maprotation.h	2019-09-04 20:11:43.864689685 +0300
@@ -0,0 +1,12 @@
+#ifndef GAME_SERVER_MAPROTATION_H
+#define GAME_SERVER_MAPROTATION_H
+
+class IMapRotation {
+ public:
+  virtual const char* NextMap(const char* rotation, const char* current) = 0;
+  virtual ~IMapRotation() {};
+};
+
+IMapRotation *RandomMapRotation();
+
+#endif
